NES Music Ripping Guide Version 1.1, May 11, 1999 ------------------------- Written by Chris Covell (ccovell@direct.ca), with help from Kevin Horton and Michel Iwaniec (among others). Contents -------- 1. Introduction 2. Early Basics 3. Getting Started i. Useful Tools 4. Spotting Music Code i. Using a HEX Editor ii. Using an Emulator 5. Post-Rip Cleanup i. Testing the Ripped Data ii. Arranging Songs iii. Ripping/Relocating Samples. 6. Conclusion 7. Appendix i. The NESM/.NSF Format ii. Example 6502 ASM Player 1. Introduction --------------- I have been asked to write up a guide on how to rip NES music for use in the latest music player for NES music files (NESM/.NSF): NESAmp, for WinAmp. Although I am not an expert by any means in music ripping, I have enough knowledge to rip the easy stuff... And to get other people started. Kevin is the one who got me started, so I am using his ideas (and some of his e-mails) as a resource. 2. Early Basics --------------- First of all, if you want to rip NES music, it is important to know several things. The most crucial is that you have a knowledge of 6502 assembly language. Get that down and memorized first. It may also be a good idea to become familiar with the HEX opcodes for the 6502, as well as the number of bytes taken up by each opcode. Next, learn hexadecimal notation, if you don't know it yet. Secondly, study the architecture of the NES/Famicom. There are several documents on this available on the Internet. You should become familiar with how the NES handles interrupts, NMI, writes to various memory areas, system vectors, and such. Become familiar especially with the sound registers located at $4000-$4015, with the exception of $4014, which is for sprites, and not sound. 3. Getting Started ------------------ To begin ripping NES music, it's a good idea to have both the physical NES cartridge and a ROM file of it. Not only is this a good idea for legal reasons, but the "real thing" is your control system. It's where the NES game will behave exactly as it was designed. Unfortunately, no emulator is perfect. i. Useful Tools ---------------- Having many emulators for your specific computer(s) is very useful. They all behave differently with different ROMs, so knowing each emulator's strengths and weaknesses is good. Here is what I have going on my computers (I rip and assemble NES music and demos on my Amiga) Amiga: A/NES: Good for fast NTSC emulation, and somewhat more compatible. Displays NES vectors. Good for listening to the DPCM samples REALLLY LOUD. :-) CoolNESs: Better sound than A/NES. MacOS: GrayBox: Great for viewing areas in memory while a game is running, even areas in memory which are supposed to be "write-only". Ideal for spotting writes to the sound registers and especially to the DPCM registers. PC: Nesticle: Not the most compatible, but very useful for tracing through a game, and dumping the active NES memory to a file. FWNES: Useful also for searching through memory, also more compatible with games. It is also useful for whatever computer you use to have a HEX editor, a 6502 disassembler, a split/join utility like cat or join, and perhaps even a 6502 assembler. A good stereo system for listening for errors in sound, and lots of time and patience are also useful. If you are successful at ripping music, you might want to get a program that searches for relative text, in order to search for the music composer's credits in the game ROM. This is if you don't know the composer's name off-hand, and don't want to have to play through the game just to see the composer's name in the ending credits. A program called Hexposure exists for DOS, and I have made a program called ROMSearcher which is written in C++. Binaries of it exist for DOS, Windows, Linux, and AmigaOS. 4. Spotting Music Code ---------------------- Okay, we're starting. Most NES music accesses its music-routines in a logical way, by first JSRing to an INIT subroutine, which sets up areas in memory and sets a song going. Next, every VBlank, the game JSRs to its PLAY subroutine, which keeps updating the audio hardware, making music. So: INIT once to start music, PLAY many times to play the music. Some games behave in a nonstandard way, such as not having an INIT subroutine to speak of, etc... You will have to deal with these yourself. i. Using a HEX Editor --------------------- Sometimes, you don't even need an emulator to spot the music code. First, load up the game ROM in a HEX editor, and search for a write to the NES' sound address space. Generally, I do a search for $8D$15$40, which is "STA $4015" in ASM speak. Usually these writes will turn on or off the sound channels, which is a good indicator of code which is part of the INIT routine. Now the tough part is to trace through the code to figure out where the INIT subroutine actually begins. If you are using a HEX editor, you should take note of the way many NES games are laid out in a ROM. In games that have more than 32k of game code, mappers are used to swap in portions of code. Often, the music code stays in memory all the time, while game code gets swapped in. Sometimes, the music code is enmeshed in the game code, and so it will be your job to separate that. But anyhow, let's imagine that there is a game which is larger than 32k, and it has its music code in memory at all times. Usually, the music code resides in the $8000-$BFFF region, and the game code is swapped in the $C000-$FFFF region. This means that you may have to inspect each 16k section in the game ROM for music code. First, get rid of the NES header so that the addresses align correctly. If you search for that write to $4015 in a HEX editor, and a search pops up, keep searching. It may not be the only write, and it may not even be limited to a 16k chunk of memory. (There may be several 16k chunks of music code in a game, like what Kevin encountered when ripping Kirby's Adventure and Metal Gear 2.) If a search pops up very close to a 16k offset in the ROM file, like at $0000, $4000, $8000, $C000, $10000, $14000, etc.. then you're quite in luck. Chances are that the music code begins very close to the start of that 16k section. Copy out that entire 16k chunk of the ROM into another file. I usually call that "*.mus". So you now have what is most likely the music code for a particular game. You may want to search through that file for the likely starting point of the INIT routine, or disassemble the chunk of code (with ORIGIN $8000 or whatever) and trace through it for the INIT and even PLAY routines, if you wish. That may be enough, but if not, it might be a good idea to search through the game code of the ROM for calls to the INIT or PLAY routines. Generally, the PLAY routine is called in the middle of a game's NMI code. The vector for the NMI routine is located at $FFFA-$FFFB in the NES memory, and is generally the 6th and 5th to the last bytes before a 16k offset in game code. This NMI routine should point to some place in the $C000-$FFFF region of memory, but some games do point to weird places, like $0700, or $6000, or even somewhere after $8000. When you have found what you think is the NMI vector, go to the location in the file addressed by the vector. If the potential NMI code starts out with stuff like "PHA, TXA, PHA, TYA, PHA, etc.." it is probably the beginning of the NMI code. Now, somewhere in the NMI code, the game should JSR to a subroutine which is the PLAY code. Look for a JSR to $8000, or $80xx. It is pretty tell-tale when a game which is greater than 32k jumps to a subroutine which is beyond a 16k boundary. You know that you have found the PLAY code when the subroutine in this memory region which the NMI code calls does a bunch of saves to $4000-$4013, or calls other subroutines which save to $4000-$4013. Next, to find the INIT code, well, that is more difficult. Use what you found when you did the search for "STA $4015" to help you. Try to trace backwards through the music code for possible entry-points into an INIT routine. Usually, they will directly follow an RTS instruction, or opcode $60. Sometimes when I'm stumped, I just try out all the points in the music code that follow an RTS. You can also trace through the game code for calls to this elusive INIT routine; the calls for it might be in the NMI routine, or even in the RESET routine, pointed to by $FFFC-$FFFD in the NES' memory, which is just after the NMI vector. Trace, trace, trace; that's all you can do. Let's just say that the best-case scenario is many Capcom and Sunsoft games. They have their music code at $8000-$BFFF, their INIT routine is at $8003, and the PLAY routine is at $8000. Couldn't be simpler. Of course, there are many exceptions. Using an emulator can help out with those... ii. Using an Emulator --------------------- Here is what Kevin has to say about using an emulator: *BEGIN QUOTE* I have used the following method to rip stuff very effectively: (assuming NESticle here) 1) start up the victim, earm, game. 2) pause it. [pause emulation; don't just press "start"] 3) go into debug and hit "VBI" [it's "Vint" in Nesticle] 4) keep hitting control-T to trace the code until you see a tell-tale write. 5) go back into debug and hit "save RAM" Now, you've got a conveniently extracted chunk of code in the correct space in memory and everything. Lop off the lower 32K and keep the upper 32K. This should be a complete rip of the music. Now you've got a much smaller chunk of code to peruse thru. Also remember to write down the address that was called to the sound area. (note this may take a few goes... don't be afraid to re-run the code to find it) This will only tell you the play address of the tune. However you can sort of deduce what the init address will be. Note that some tunes do not have an init address! For these, the tune# is stored in a memory location and the player code checks to see if it's changed, and if so it will load up the proper tune, etc. Others may need to have you store the tune # into a memory location and then call an init routine. I've seen all three and they are about equally common. [Contradiction in NESM/.NSF format document below] *END QUOTE* Remember that in NESticle you can view the messages in a scrolling window. Use this for easier viewing of all the instructions while you trace. Also note that after an RTI in the NMI code, it is a LONG time to wait until the next NMI, so trigger it yourself. If you don't have a PC or NESticle, you can still use an emulator perhaps to dump the active RAM, to trace opcodes, to trap writes to $4000-$4015, or to view the NMI, RESET, and IRQ/BRK vectors. 5. Post-Rip Cleanup ------------------- After you have extracted a chunk of memory which you believe contains music code, and you have a good idea of the LOAD, INIT, and PLAY addresses, you should test it out to see if it is a good rip. This stage is often annoying, because the correct selection of INIT and PLAY addresses can mean the difference between NES music and silence. And silence does not help you in finding out what exactly is wrong. i. Testing the Ripped Data -------------------------- There are currently two ways of playing back music ripped from NES games: in an NES ROM, or in a .NSF file. In the appendix at the bottom of this document, you will find assembly source code for the NES player, and a description of the .NSF header format. To use your rip in the NES file, you need to assemble the included source code into a 6502 code stub, and place the ripped music data where it is supposed to be in the NES ROM. First off, the source code is formatted for use with the cross-platform cross-assembler "DASM" (but it can be easily converted to other styles), and it has some constants which you must set up for the 6502 to JSR to the right locations. Be sure to define the INIT address, the PLAY address, and the maximum number of songs (defaults to $FF for now) to be played. This last constant starts at 1, not 0. So, if there are 5 songs in the music data, this constant reads 5. The start song is also definable, but you will usually want to keep this at 0 (see ii. for the reason why). Setting up the ORIGIN of the player code is important. With most rips with a load address of $8000 and a filesize of 16384 ($4000) bytes, the origin of the player code is $C000. If you have less data in the music code, or more data due to an initialization table or samples (see ii. and iii.) you will need to adjust the origin to compensate for this loss or addition of data. The key is to make sure that the music code and assembled player code add up to 32768 bytes. After you assemble the player sourcecode, you will need to make a standard iNES header (Mapper 0, 2 PRG segments, 0 CHR segments), and append to it the ripped music data, and then the binary of the assembled player code. Load up the NES ROM into an emulator, and try out the music. Press "start" to advance the songs. If the NES crashes upon playing a certain song (and not at the beginning of the ROM), it usually means that it is trying to play an invalid song (it is pointing to a non-existent song). In the assembler code, change the starting song constant to see if there are more valid songs after that invalid song. The other way to test out the ripped music data is to prepend to the data a NESM/.NSF header, and try it out in NESAmp or other(?) NSF players. The NSF header format is described in the appendix at the bottom of this document. The constants like LOAD, INIT, and PLAY address, etc, are self-explanatory. When you have successfully ripped a song and have made a working NSF file, be sure to fill in the relevant Title, Composer, and Copyright fields. Don't be too lazy or impatient to credit the composer and companies that made that great music! ii. Arranging Songs ------------------- In many NES games, the arrangement of the music and sound effects may be helter-skelter. Sometimes, a game may have its "ending" music before its level music, or it might have a section of music, then sound effects, then more music. This makes for inconvenient listening. What the ripper can do is rearrange the ordering of the "songs" so that the songs are arranged in a more logical fashion. It's easier than you think! All that one has to do is make up a table (or array or index) containing a "proper" ordering of songs in the music file. Let's say for example that when you play the music of a certain game, the order of songs encountered in the music-player (either in the NES ROM or the .NSF file) goes like this: Title, Ending, Level 2, Level 1, Level 3. You obviously want to rearrange the songs so that the Level music is in the correct order, and that the Ending music is at the end. In the NES ROM of the assembled sourcecode, you can see a pair of numbers on-screen which tell you the current song number being played. (This corresponds to the number which is loaded into the 6502's accumulator right before JSRing to the INIT subroutine.) The way the hypothetical ROM is right now, song 0 is the Title music, song 1 the Ending music, song 2 the Level 2 music, and so on. What I usually do is go through each song, and write down the song number and what part in the game each song is from, like I did above. I then make an ordered list of songs. In the case of the example above, I would write down $00,$03,$02,$04,$01. There, we have the 5 songs, all in the order that you want. Now we have to modify the music data to reflect this. First off, locate at least 8 or so empty bytes in the music data. These are regions which you know are unused, and are filled with 0s or FFs, or whatever. Just make sure that these are UNUSED bytes. If you can't find any, then you will just have to append the relocation data at the end of the music data file, and adapt the Origin of the NES player code (if you are using it) to reflect those changes. The 8 or so bytes are going to be used up with some instructions designed to load the accumulator with a number from that ordered list of songs that you made. But first, you need to check the first instructions at the beginning of the INIT code (which you discovered in step 4), to see if they are relocatable. Usually, the first bytes immediately at the INIT address are something to the effect of $4C$zz$zz, in other words, JMP $zzzz. This is really easy to modify. Simply change in a HEX editor the address in the JMP instruction to the beginning of the location where you found some free bytes. Make sure you memorize or jot down the original address JMPed to in that instruction. Also make sure you take into account the origin of the music code. For example, whatever offset you have in the music code file, you must add $8000 to it (or whatever your Origin/LOAD address) is. Next, you should look for some more empty and unused bytes to store your relocation table. You will need as many free bytes as there are songs in your list of music. In the example above, I had 5 songs, so I will need 5 unused bytes. I will then type in the list of songs into these bytes: $00,$03,$02,$04,$01. Now, memorize or write down the location of the beginning of this table (being sure to add the origin to this number as well.) Now that you know the address of the relocation table, you should lay down some HEX code back at those 8 or so unused bytes: $AA,$BD,$yy,$xx,$4C,$zz,$zz, which translates to: TAX LDA $xxyy,X JMP $zzzz (where $xxyy is the address of your relocation table, the area where your ordered list of music is stored; and zzzz is the original address which was originally at the beginning of the INIT routine). What this will do is: The NSF player/NES ROM player will JSR to the same old INIT subroutine with the starting song stored in the accumulator, but as soon as it gets there, it will be redirected through a JMP instruction to your relocation code. Your relocation code takes the value stored in the accumulator and uses that to get values from the lookup-table that you made. Once it has this value, it will JMP back to the REAL beginning of the INIT code. And voila! You have made a simple relocation table! Try it out to see if it works. Sometimes, at the beginning of the INIT routine in a game, the code is something complicated, like $C9,$FD,$D0,$03.... or in ASM terms, CMP #$FD BNE ($INIT+#$04) + #$03 ;Remember that BNEs are relative, not absolute! This might trip you up, but you can always put this code after your relocation routine. In place of this code at the location of the INIT routine, put this instead: $4C,$ww,$vv,$EA, which is: JMP $vvww ;location of relocation code. NOP ;to fill in the gap. Next, at the location for your relocation code, put in: $AA,$BD,$yy,$xx,$C9,$FD,$D0,$03,$4C,($INIT+#$04),$4C,($INIT+#$04)+#$03, which is: TAX LDA $xxyy,X CMP #$FD BNE (PC+1) + 3 ;Branch 3 bytes over (after PC increment) JMP ($INIT+#$04) ;Whatever was skipped over back at INIT, in other words. JMP ($INIT+#$04) + 3 (This code might look messy. I'll give an example. Mega Man 2 is like this. At its INIT routine at $8003, it has this code: $C9,$FC,$D0,$03,$4C,$29,$81,$C9,... which in ASM is this: CMP #$FC BNE $800A JMP $8129.... I replace it with a JMP to my redirection code, and at my redirection code is this: $AA,$BD,$yy,$xx,$C9,$FC,$D0,$03,$4C,$07,$80,$4C,$0A,$80, which in ASM is this: TAX LDA $xxyy,X ;Get value from lookup-table. CMP #$FC BNE (PC+1) + 3 JMP $8007 ;This is the location directly after the BNE #$03... JMP $800A ;This is the location it would have jumped to. This may seem like a lot of hard work, but it is an efficient way of relocating code and still maintaining the same logic structure of the original music data. Better still, the INIT and PLAY routine locations remain the same as they were in the unmodified version of the music data, which is important for consistency's sake. I realize that this may sound confusing in description, but believe me, it is simple in practice. iii. Ripping/Relocating Samples. -------------------------------- Now we come to a part that might be overlooked by hasty rippers. When you rip the music data from a game, oftentimes you might find that the DPCM samples have disappeared in the rip. If you rip the music from a game which you KNOW has samples in it, you will have to relocate the sample data located in the game code ($C000-$FFFF) and change the pointers to it in order to be able to hear the samples again in the ripped file. This method only covers DPCM samples and not relocation of the "RAW" format of samples. The latter is a little more difficult, and not something in which I have experience. First off, play a game on a real NES, and check to see if you hear any samples, such as voices, percussive instruments, etc... If it does, you will have to rip it. If you can't verify it this way, do a search through the music code of the ripped game for writes to $4010-$4013, and also somehow setting the upper nybble of $4015 to 1. Many games store the data to write to $4010-$4013 in a table, so it is your task to find this table. GrayBox on the Mac helps out immensely, because you can monitor $4010-$4015, and glean the data written to those locations. For example, let's say that a game writes to $4010-$4013 this: $0E,$00,$C0,$23. You can then search through the music code for a table containing these bytes in series, among other similar bytes. Usually, you'll find a large table with these bytes among them, which points to samples in different locations, but have only their playback frequencies ($4010) which differ. If you are just using NESticle or similar, your job is a little more difficult. Many games have the location of the sample table stored in the zero-page somewhere, so it is not a direct access to it in the ROM. It is somewhat time-consuming, but you can find the location of a sample table by disassembling the music-code, and then looking for the code where it loads a value from someplace, and then saves it at "$4010,Y". Sometimes, the zero-page address is accessed here. From that address, you can do a dump of memory at that zero-page address for a clue as to where in the music code the table is. Or, in NESticle (or a tracing emulator), you can trace through the NMI routine for the code that initiates a sample. This obviously doesn't happen every VBlank, so you might have to be creative in the timing of your traces. When you reach a point in the trace where you know beforehand (by disassembling and looking for writes to $4010-$4013) that the game is about to load data and write it to the sample addresses, slow down. Look for the first instruction that goes something like "LDA ($8D),Y". Then when it goes "STA $4010,Y" or something similar, view the registers if you can. Write down what is in the accumulator. Repeat these steps for the writes to $4011, $4012, and $4013. You now should have 4 bytes which make up part of the sample table of that game. Now do a search for these 4 bytes in the music code using a HEX editor. It should find the sample table for you! Batman, for instance, has a table that looks like this: $0F,$00,$C0,$0D $0F,$00,$C4,$1F $0F,$00,$CC,$1F $0E,$00,$CC,$1F $0C,$00,$CC,$1F This means that there are 5 different sample sounds used in the game: One at (($C0 * #$40) + $C000), which has a sample length of (($0D * #$10) + 1) (bytes), being played at a frequency of "$0F"; another sample at (($C4 * #$40) + $C000), which has a sample length of (($1F * #$10) + 1), being played at a frequency of "$0F"; and another at (($CC * #$40) + $C000), which has a sample length of (($1F * #$10) + 1), being played at 3 different frequencies: $0F, $0E, and $0C. In a table then, you have samples: Sample Location Length ----------------------------------- 1 $F000 209 (#$D1) bytes 2 $F100 497 (#$1F1) bytes 3 $F300 497 (#$1F1) bytes 4 $F300 497 (#$1F1) bytes 5 $F300 497 (#$1F1) bytes The frequency doesn't matter; just make sure you are able to count the number and length of all of the samples used in the game. Most games store their sample data at or near the end of their game code, at around $E000-$FFFF. Once you have taken note of all the samples used (either by studying the table in the music code of a game, or by taking note of all the different combinations of things written to $4010-$4013, save the total sequential sample data pointed to by the game into a different file, maybe called "game.smp". In Batman, for instance, I would rip out the data from $F000 (in the NES' memory space) to $F4F1 (at least) and save it into a file. Maybe making the total sample length a multiple of 64 bytes (but still longer than the used sample length) would be a good idea. Now that you have the samples correctly stored in another file, you need to tack it on to the end of the music code that you ripped. But, you need to make sure that the sample data fits into the address space of $C000-$CFFF in the NES, AND that the sample data is aligned at a 64-byte boundary. If you just rip out the music code, and the total length of the code is a full 16384 bytes, then the sample data appended to the music code will show up exactly at $C000. If the music code that you ripped does not go all the way to $BFFF in the NES' memory space, then you will need to pad the rest of the music code to make up for the shortage. SAMPLES NEED TO FIT SOMEWHERE IN $C000-$FFFF!! After you have chosen a good location for the sample data, you will need to modify the instances of all those samples in the sample table to reflect your putting the samples at the beginning of $C000 instead of where it was at the end of memory. For instance in Batman, the sample which was at $F000 is now at $C000. So you will need to change the "sample address" byte (which will go into $4012) in the sample table to $00, instead of the $C0 which it once was. Clever hexadecimal mathematicians will realize that you now have to do the same to the rest of the instances in the sample table: you have to subtract $C0 from the other sample pointers as well. A finished sample table (in Batman, for example) should look like the one on the right: $0F,$00,$C0,$0D <- this $0F,$00,$00,$0D $0F,$00,$C4,$1F goes to $0F,$00,$04,$1F $0F,$00,$CC,$1F this -> $0F,$00,$0C,$1F $0E,$00,$CC,$1F --> $0E,$00,$0C,$1F $0C,$00,$CC,$1F --> $0C,$00,$0C,$1F It is a little tricky to grasp, but if you just study the meaning of the registers from $4010 - $4015, you should understand it. I can provide other example files for better illustrative purposes. So, to recap... Hunt for and rip out the sample data. Stick the sample data somewhere which will go into $C000-$FFFF of the NES' memory at a 64-byte offset. Change the bytes in the sample table which point to the location of the sample data to point to the new location of the samples. Finally, if you are using the ASM NES player to make the ripped music code into an NES ROM, make sure you: Make a valid iNES header, append the music code, append the sample data, assemble the ASM file (MAKING SURE TO CHANGE THE ORIGIN OF THE PLAYER CODE TO REFLECT THE NEW PRESENCE OF THE SAMPLE DATA STARTING AT THE $C000 REGION!), and append the assembled binary file. You should have a working, sampling :-) NES audio ROM. For .NSF files, just make sure you make a valid .NSF header, append the music code, and then append the sample data (making sure the sample data will get loaded in to somwehere in $C000-$FFFF!) 6. Conclusion ------------- This is just a guide to the basics of NES music ripping. I'm sure I have not covered some things here which are also necessary for ripping some games, but again, my knowledge of this matter is definitely not extensive. This document is rather large, and I have written a whole lot. Don't be confused or bewildered initially by all the damn writing. Get to know ASM and the NES internals first; the terminology that I use will then become a little more understandable. I must give a big THANK YOU to Kevin Horton, who invented the .NSF header format, and started the ball rolling on NES music. He still does all the difficult rips; I have done just some easy ones. Thanks also to Michel Iwaniec for making some great NSF players. I look forward to the day when we will have a collection that rivals the HVSC (just kidding!) If anybody has any questions about NES music ripping that isn't covered (adequately) in this document, feel free to mail me at ccovell@direct.ca. Also, if you spot any glaring errors or omissions, let me know. You might also want to take the time to check out my webpage (when it is back in service) at http://mypage.direct.ca/c/ccovell, for some fun videogame-related stuff, and art and poetry, and much more! Bye for now and good luck ripping!!! 7. Appendix ----------- i. The NESM/.NSF Format ----------------------- The NESM format is a header format invented by Kevin Horton and designed to enable music players to play back music code ripped from NES and Famicom games, using a standardized interface. The format is called NESM, but the files themselves have the .NSF suffix, which stands for "NES Sound Format". Around the Internet, most people are already beginning to refer to these files as just "NSFs". I believe that is the name that will stick. In this document, I refer to files in this format as such. Here is Kevin's NESM/.NSF proposal, along with modifications for PAL/NTSC and Bankswitching. NES Music Format Spec --------------------- By: Kevin Horton khorton@iquest.net V1.00 *First official NSF specification file* This file encompasses a way to transfer NES music data in a small, easy to use format. The basic idea is one rips the music/sound code from an NES game and prepends a small header to the data. A program of some form (6502/sound emulator) then takes the data and loads it into the proper place into the 6502's address space, then inits and plays the tune. Here's an overview of the header: offset # of bytes Function ---------------------------- 0000 5 STRING "NESM",01Ah ; denotes an NES sound format file 0005 1 BYTE version number (currently 01h) 0006 1 BYTE total songs (1=1 song, 2=2 songs, etc) 0007 1 BYTE starting song (1= 1st song, 2=2nd song, etc) 0008 2 WORD (lo/hi) load address of data (8000-FFFF) 000a 2 WORD (lo/hi) init address of data (8000-FFFF) 000c 2 WORD (lo/hi) play address of data (8000-FFFF) 000e 32 STRING The name of the song, null terminated 002e 32 STRING The artist, if known, null terminated 004e 32 STRING The Copyright holder, null terminated 006e 2 WORD (lo/hi) speed, in 1/1000000th sec ticks, NTSC (see text) 0070 8 BYTE Bankswitch Init Values (see text) 0078 2 WORD (lo/hi) speed, in 1/1000000th sec ticks, PAL (see text) 007a 1 BYTE PAL/NTSC bits: bit 0: if clear, this is an NTSC tune bit 0: if set, this is a PAL tune bit 1: if set, this is a dual PAL/NTSC tune bits 2-7: not used. they *must* be 0 007b 5 ---- 5 extra bytes for expansion (must be 00h) 0080 nnn ---- the music program/data follows This may look somewhat familiar; if so that's because this is somewhat sorta of based on the PSID file format for C64 music/sound. Loading a tune into RAM ----------------------- If offsets 0070h to 0077h have 00h in them, then bankswitching is *not* used. If one or more bytes are something other than 00h then bankswitching is used. If bankswitching is used then the load address is still used, but you now use (ADDRESS AND 0FFFh) to determine where on the first bank to load the data. Each bank is 4K in size, and that means there are 8 of them for the entire 08000h-0ffffh range in the 6502's address space. You determine where in memory the data goes by setting bytes 070h thru 077h in the file. These determine the inital bank values that will be used, and hence where the data will be loaded into the address space. Here's an example: METROID.NSF will be used for the following explaination. The file is set up like so: (starting at 070h in the file) 0070: 05 05 05 05 05 05 05 05 - 00 00 00 00 00 00 00 00 0080: ... music data goes here... Since 0070h-0077h are something other than 00h, then we know that this tune uses bankswitching. The load address for the data is specified as 08000h. We take this AND 0fffh and get 0000h, so we will load data in at byte 0 of bank 0, since data is loaded into the banks sequentially starting from bank 0 up until the ROM is fully loaded. Metroid has 6 4K banks in it, numbered 0 through 5. The 6502's address space has 8 4K bankswitchable blocks on it, starting at 08000h-08fffh, 09000h-09fffh, 0a000h-0afffh ... 0f000h-0ffffh. Each one of these is 4K in size, and the current bank is controlled by writes to 05ff8h thru 05fffh, one byte per bank. So, 05ff8h controls the 08000h-08fffh range, 05ff9h controls the 09000h-09fffh range, etc. up to 05fffh which controls the 0f000h-0ffffh range. When the song is loaded into RAM, it is loaded into the banks and not the 6502's address space. Once this is done, then the bank control registers are written to set up the inital bank values. To do this, the value at 0070h in the file is written to 05ff8h, 0071h is written to 05ff9h, etc. all the way to 0077h is written to 05fffh. This is only done once, when the song is loaded. It is not done after the song is loaded between each tune init so make sure that the rip takes this into account (the rip usually will). If the tune was not bankswitched, then it is simply loaded in at the specified load address, until EOF Initalizing a tune ------------------ This is pretty simple. Load the desired song # into the accumulator, minus 1 and set the X register to specify PAL (X=1) or NTSC (X=0). If this is a single standard tune (i.e. PAL *or* NTSC but not both) then the X register contents should not matter. Once the song # and optional PAL/NTSC standard are loaded, simply call the INIT address. Once init is done, it should perform an RTS. Playing a tune -------------- Once the tune has been initalized, it can now be played. To do this, simply call the play address several times a second. How many times per second is determined by offsets 006eh and 006fh in the file. These bytes denote the speed of playback in 1/1000000ths of a second. For the "usual" 60Hz playback rate, set this to 411ah. To generate a differing playback rate, use this formula: 1000000 PBRATE= --------- speed Where PBRATE is the value you stick into 006e/006fh in the file, and speed is the desired speed in hertz. "Proper" way to load the tune ----------------------------- 1) If the tune is bankswitched, go to #3. 2) Load the data into the 6502's address space starting at the specified load address. Go to #4. 3) Load the data into a RAM area, starting at (start_address AND 0fffh). Load the inital bank values into the bank select registers. 4) Tune load is done. "Proper" way to init a tune --------------------------- 1) Clear all RAM at 0000h-07ffh. 2) Init the sound registers by writing 00h to 04000-04013h. 3) Set volume register 04015h to 00fh. 4) Set the accumulator and X registers for the desired song. 5) Call the music init routine. "Proper" way to play a tune --------------------------- 1) Call the play address of the music at periodic intervals determined by the speed words. Which word to use is determined by which mode you are in- PAL or NTSC. Caveats ------- 1) The starting song number and maximum song numbers start counting at 1, while the init address of the tune starts counting at 0. To "fix", simply pass the desired song number minus 1 to the init routine. 2) The NTSC speed word is used *only* for NTSC tunes, or dual PAL/NTSC tunes. The PAL speed word is used *only* for PAL tunes, or dual PAL/NTSC tunes. 3) The length of the text in the name, artist, and copyright fields must be 31 characters or less! There has to be at least a single NULL byte (00h) after the text, between fields. 4) If a field is not known (name, artist, copyright) then the field must contain the string "" (without quotes). That's it! ii. Example 6502 ASM Player --------------------------- This is the sourcecode for a 6502 player which will play ripped NES music. It was coded by Kevin Horton, and I converted it to DASM's format and added a graphical display and definable constants for INIT, PLAY, etc... You can assemble it in the cross-platform cross-assembler DASM, using the option -f3, and by joining an iNES header, the ripped music code, and the assembled binary together. It should in its present form, with header, ripped music, and binary, form a .NES file which is 32784 bytes large. Press "start" to advance songs. ;*BEGIN ASM FILE* int_en EQU #$0100 sng_ctr EQU #$0101 pv_btn EQU #$0102 INIT_ADD EQU #$8003 ;INIT Address PLAY_ADD EQU #$8000 ;PLAY Address START_SONG EQU #$00 ;Starting Song MAX_SONG EQU #$FF ;Maximum Number of Songs PROCESSOR 6502 ;code ORG $C000 ;Origin of player code .start sei cld ldx #$ff txs .w_vbi lda $2002 bpl .w_vbi lda #$0 tax .ci_lp sta $0000,X sta $0100,X sta $0200,X sta $0300,X sta $0400,X sta $0500,X sta $0600,X sta $0700,X inx bne .ci_lp ;clear RAM lda #$80 sta $2000 lda #$00 sta $2001 ;set up PPU for interrupts, disable screen lda #$00 sta $2006 sta $2006 tax .PATCLR sta $2007 ;Clear char 0 in ppu. inx cpx #$10 bne .PATCLR lda #$01 sta $2006 lda #$00 sta $2006 ;Point to $0100 in PPU ldx #$00 ldy #$00 .PATLoad lda .PAT,X sta $2007 inx iny cpy #$08 ;Save 8 bytes. bne .PATLoad ldy #$00 sty $2007 ;Save blank gfx. sty $2007 sty $2007 sty $2007 sty $2007 sty $2007 sty $2007 sty $2007 cpx #$80 bne .PATLoad lda #$3F sta $2006 lda #$00 sta $2006 lda #$0E sta $2007 lda #$30 sta $2007 ;Set up Palette lda #$00 sta sng_ctr lda #%00001110 sta $2001 lda #START_SONG ;Start Song sta sng_ctr jsr INIT_ADD ;init tune LDA #$01 sta int_en .k_loop jsr .r_btn and #$10 beq .k_loop inc sng_ctr LDA #MAX_SONG ;Max Song. cmp sng_ctr bne .no_scr lda #$0 sta sng_ctr .no_scr lda #$0 sta int_en lda sng_ctr jsr INIT_ADD lda #$01 sta int_en jmp .k_loop ;check button, if pressed inc song # and re-init .interrupt pha txa pha tya pha lda #$20 sta $2006 sta $2006 ;Point to Name Table lda sng_ctr lsr lsr lsr lsr ora #$10 ;Point to right chr. sta $2007 lda sng_ctr and #$0F ora #$10 sta $2007 lda #$00 sta $2006 sta $2006 sta $2005 sta $2005 lda int_en beq .no_ints jsr PLAY_ADD ;play tune .no_ints pla tay pla tax pla rti .r_btn ldy #$08 ;read keypad ldx #$01 stx $4016 dex stx $4016 .r_bit lda $4016 ROR txa ROL tax dey bne .r_bit cmp pv_btn beq .no_chg sta pv_btn rts .no_chg lda #$0 rts .PAT dc.b #$38,#$4C,#$C6,#$C6,#$C6,#$64,#$38,#$00 dc.b #$18,#$38,#$18,#$18,#$18,#$18,#$7E,#$00 dc.b #$7C,#$C6,#$0E,#$3C,#$78,#$E0,#$FE,#$00 dc.b #$7E,#$0C,#$18,#$3C,#$06,#$C6,#$7C,#$00 dc.b #$1C,#$3C,#$6C,#$CC,#$FE,#$0C,#$0C,#$00 dc.b #$FC,#$C0,#$FC,#$06,#$06,#$C6,#$7C,#$00 dc.b #$3C,#$60,#$C0,#$FC,#$C6,#$C6,#$7C,#$00 dc.b #$FE,#$C6,#$0C,#$18,#$30,#$30,#$30,#$00 dc.b #$7C,#$C6,#$C6,#$7C,#$C6,#$C6,#$7C,#$00 dc.b #$7C,#$C6,#$C6,#$7E,#$06,#$0C,#$78,#$00 dc.b #$38,#$6C,#$C6,#$C6,#$FE,#$C6,#$C6,#$00 dc.b #$FC,#$C6,#$C6,#$FC,#$C6,#$C6,#$FC,#$00 dc.b #$3C,#$66,#$C0,#$C0,#$C0,#$66,#$3C,#$00 dc.b #$F8,#$CC,#$C6,#$C6,#$C6,#$CC,#$F8,#$00 dc.b #$FE,#$C0,#$C0,#$FC,#$C0,#$C0,#$FE,#$00 dc.b #$FE,#$C0,#$C0,#$FC,#$C0,#$C0,#$C0,#$00 ;fill empty space ORG $FFFA,0 ;vectors dc.w .interrupt dc.w .start dc.w .interrupt ;*END ASM FILE* -- Chris Covell (ccovell@direct.ca) http://mypage.direct.ca/c/ccovell/ Powered by Amiga!