/* * HalfNES by Andrew Hoffman * Licensed under the GNU GPL Version 3. See LICENSE file */ package com.grapeshot.halfnes.mappers; import com.grapeshot.halfnes.*; import com.grapeshot.halfnes.audio.*; import java.util.Arrays; /** * * @author Andrew */ public class NSFMapper extends Mapper { //a nsf playing mapper //TODO: add the extra bankswitches required when playing FDS private int load, init, play, song, numSongs; public boolean nsfBanking; public int[] nsfStartBanks = new int[10], nsfBanks = new int[10]; private int sndchip; boolean vrc6 = false, vrc7 = false, mmc5 = false, n163 = false, s5b = false, hasInitSound = false, fds = false; private boolean n163autoincrement = false; private int n163soundAddr = 0; private int mmc5multiplier1, mmc5multiplier2; private int vrc7regaddr = 0; private int s5bSoundCommand = 0; private Namco163SoundChip n163Audio; private VRC6SoundChip vrc6Audio; private VRC7SoundChip vrc7Audio; private Sunsoft5BSoundChip s5bAudio; private MMC5SoundChip mmc5Audio; private static final String trackstr = "Track --- / --- <-B A->"; private FDSSoundChip fdsAudio; @Override public void loadrom() throws BadMapperException { loader.parseHeader(); prgsize = loader.prgsize; mappertype = loader.mappertype; prgoff = loader.prgoff; for (int i = 0x70; i < 0x78; ++i) { if (loader.header[i] != 0) { nsfBanking = true; nsfStartBanks[i - 0x70] = loader.header[i]; } } prgoff = 0; load = loader.header[0x08] + (loader.header[0x09] << 8); init = loader.header[0x0a] + (loader.header[0x0b] << 8); play = loader.header[0x0c] + (loader.header[0x0d] << 8); numSongs = loader.header[6] - 1; song = loader.header[7] - 1; if (loader.header[0x7a] == 1) { //pal only tune this.region = TVType.PAL; System.err.println("pal only tune"); } else { this.region = TVType.NTSC; } chroff = 0; chrsize = 0; scrolltype = MirrorType.V_MIRROR; sndchip = loader.header[0x7B]; if (!nsfBanking && load < 0x8000) { //no banking System.err.println("What do I do with this???"); throw new BadMapperException("NSF with no banking loading low"); } // pad to 4k bank size and copy in starting //from where the load addr is in a 4k bank //to the end of the file, padding the end to a 4k bank as well //so total number of banks can be 2 more than # of 4k //chunks in the file. int paddingLen = (nsfBanking) ? load & 0x0fff : load - 0x8000; prg = new int[1024 * 1024]; System.arraycopy(loader.load(loader.romlen(), prgoff), 0, prg, paddingLen, loader.romlen()); crc = crc32(prg); haschrram = true; chrsize = 8192; chr = new int[8192]; prg_map = new int[(((sndchip & (utils.BIT2)) != 0)) ? 40 : 32]; if (!nsfBanking) { //identity mapping from 1st loaded bank for (int i = 0; i < 8; ++i) { nsfStartBanks[i] = i; } } //additional headache for NSFs with FDS: if (((sndchip & (utils.BIT2)) != 0)) { //got to copy some stuff into 6000 - 7fff just because nsfStartBanks[8] = nsfStartBanks[6]; nsfStartBanks[9] = nsfStartBanks[7]; } chr_map = new int[8]; for (int i = 0; i < 8; ++i) { chr_map[i] = (1024 * i) & (chrsize - 1); } cpuram = new CPURAM(this); cpu = new CPU(cpuram); ppu = new PPU(this); Arrays.fill(pput0, 0x00); setmirroring(scrolltype); //System.out.println(sndchip); //set up the PPU to display titles //pick a random color based on the tune's crc (why not?) ppu.pal[0] = 0x3f; ppu.pal[1] = 0x20 + (int) (crc % 12); ppu.pal[2] = 0x20 + (int) (crc % 12); ppu.pal[3] = 0x20 + (int) (crc % 12); chr = NSFPlayerFont.font; } @Override public void init() { //now that we've set up the initial CPU state, do it all over again //in order to match the NSF spec. //set banks back to the way they were originally nsfBanks = nsfStartBanks.clone(); setBanks(); //clear all ram to 0 for (int i = 0; i <= 0x7ff; ++i) { cpuram.write(i, 0); } //initialize sound registers for (int i = 0x4000; i <= 0x4013; ++i) { cpuram.write(i, 0); } cpuram.write(0x4015, 0x0f); //disable frame counter on APU cpuram.write(0x4017, 0x40); //simulate a jump to the play address cpu.push(0xff); cpu.push(0xfa); cpu.setPC(init); cpu.interrupt = -99999; //no interrupts for you cpu.setRegA(song); if (this.region == TVType.PAL) { cpu.setRegX(0x01); } else { cpu.setRegX(0x00); } //copy titles to ppu nametable for (int i = 0; i < 32 * 24; ++i) { //random pattern from basic one liner pput0[i] = (Math.random() > 0.5) ? 0x2f : 0x5c; } for (int i = 0; i < 96; ++i) { pput0[i + (32 * 25)] = loader.header[i + 0xe]; } for (int i = 0; i < trackstr.length(); ++i) { pput0[i + (32 * 28)] = trackstr.charAt(i); } if (!hasInitSound) { setSoundChip(); hasInitSound = true; } if (!fds) { //DON'T CLEAR THIS WHEN STUFF LOADS HERE for (int i = 0x6000; i <= 0x7fff; ++i) { cpuram.write(i, 0); } } } @Override public void reset() { song = loader.header[7] - 1; init(); cpu.setPC(init); } //write into the cartridge's address space @Override public void cartWrite(final int addr, final int data) { if (n163 && addr == 0xF800) { n163autoincrement = ((data & (utils.BIT7)) != 0); n163soundAddr = data & 0x7f; } else if (n163 && addr == 0x4800) { n163Audio.write(n163soundAddr, data); if (n163autoincrement) { n163soundAddr = ++n163soundAddr & 0x7f; } } else if (s5b && addr == 0xE000) { s5bAudio.write(s5bSoundCommand, data); } else if (s5b && addr == 0xC000) { s5bSoundCommand = data & 0xF; } else if (vrc6 && addr >= 0xB000 && addr <= 0xB002) { vrc6Audio.write((addr & 0xf000) + (addr & 3), data); } else if (vrc6 && addr >= 0xA000 && addr <= 0xA002) { vrc6Audio.write((addr & 0xf000) + (addr & 3), data); } else if (vrc7 && addr == 0x9030) { vrc7Audio.write(vrc7regaddr, data); } else if (vrc7 && addr == 0x9010) { vrc7regaddr = data; } else if (vrc6 && addr >= 0x9000 && addr <= 0x9002) { vrc6Audio.write((addr & 0xf000) + (addr & 3), data); } else if (fds && nsfBanking && addr >= 0x6000) { if (addr < 0x8000) { int fuuu = prg_map[((addr - 0x6000) >> 10) + 32] + (addr & 1023); prg[fuuu] = data; } else { int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023); prg[fuuu] = data; } } else if (fds && !nsfBanking && addr >= 0x6000) { if (addr < 0x8000) { prgram[addr - 0x6000] = data; } else { int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023); prg[fuuu] = data; } } else if (addr >= 0x6000 && addr < 0x8000) { //default no-mapper operation just writes if in PRG RAM range prgram[addr & 0x1fff] = data; } else if ((addr >= 0x5ff8) && (addr < 0x6000)) { nsfBanks[addr - 0x5ff8] = data; //System.err.println(addr - 0x5ff8 + " " + data); setBanks(); } else if (fds && nsfBanking && (addr == 0x5ff6)) { //System.err.println("fds request bank " + data + " in ram0"); nsfBanks[8] = data; setBanks(); } else if (fds && nsfBanking && (addr >= 0x5ff7)) { //System.err.println("fds request bank " + data + " in ram1"); nsfBanks[9] = data; setBanks(); } else if (mmc5 && (addr >= 0x5C00) && (addr <= 0x5FF5)) { prgram[addr - 0x5C00] = data; //RAM emulates ExRAM here } else if (mmc5 && (addr == 0x5206)) { mmc5multiplier2 = data; } else if (mmc5 && (addr == 0x5205)) { mmc5multiplier1 = data; } else if (mmc5 && (addr >= 0x5000) && (addr <= 0x5015)) { mmc5Audio.write(addr - 0x5000, data); } else if (fds && (addr >= 0x4040) && (addr <= 0x4092)) { fdsAudio.write(addr, data); } else { System.err.println("write to " + utils.hex(addr) + " goes nowhere"); } } @Override public int cartRead(final int addr) { // by default has wram at 0x6000 and cartridge at 0x8000-0xfff // but some mappers have different so override for those if (addr >= 0x8000) { if (addr > 0xfffa) { //reads of last part of RAM should always //give the reset vectors here, no matter what //NSF bank is mapped there. switch (addr) { case 0xfffb: return 0x4c; case 0xfffc: return 0xfb; case 0xfffd: return 0xff; default: return 0x00; } } int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023); return prg[fuuu]; } else if (addr >= 0x6000 && hasprgram) { if (fds && nsfBanking) { int fuuu = prg_map[((addr - 0x6000) >> 10) + 32] + (addr & 1023); return prg[fuuu]; } else { return prgram[addr & 0x1fff]; } } else if ((addr >= 0x5ff8)) { return nsfBanks[addr - 0x5ff8]; } else if (fds && nsfBanking && (addr == 0x5ff6)) { return nsfBanks[8]; } else if (fds && nsfBanking && (addr == 0x5ff7)) { return nsfBanks[9]; } else if (mmc5 && addr >= 0x5C00) { return prgram[addr - 0x5C00]; //RAM emulates ExRAM here } else if (mmc5 && addr == 0x5206) { return ((mmc5multiplier1 * mmc5multiplier2) >> 8) & 0xff; } else if (mmc5 && addr == 0x5205) { return (mmc5multiplier1 * mmc5multiplier2) & 0xff; } else if (mmc5 && addr == 0x5015) { return mmc5Audio.status(); } else if (n163 && addr == 0x4800) { System.err.println("readback"); int retval = n163Audio.read(n163soundAddr); if (n163autoincrement) { n163soundAddr = ++n163soundAddr & 0x7f; } return retval; } else if (fds && (addr >= 0x4040) && (addr < 0x4093)) { return fdsAudio.read(addr); } System.err.println("reading open bus " + utils.hex(addr)); return addr >> 8; //open bus } @Override public void ppuWrite(int addr, final int data) { } int control, prevcontrol; int unfinishedcounter = 0; int time = 4; @Override public void notifyscanline(final int scanline) { if (scanline == 240) { //make sure init isn't still running if (cpu.PC != 0xFFFB) { //if not in idle loop if (unfinishedcounter < time) { ++unfinishedcounter; System.err.println("Init routine hasn't returned in " + unfinishedcounter + " frames"); return; } else if (unfinishedcounter == time) { ++unfinishedcounter; System.err.println("giving up"); } //if we've given it a few frames //and it still hasn't returned from init, then it probably //isn't going to (supernsf) //so we move blithely forward. } else { unfinishedcounter = 0; } //set PPU registers to enable rendering ppu.write(6, 0); ppu.write(6, 0); ppu.write(5, 0); ppu.write(0, 0); ppu.write(1, utils.BIT1 | utils.BIT3 | utils.BIT4); //write track number to screen writeTracks(); //todo: visualization effects //read the controller prevcontrol = control; control = 0; //strobe cpuram.write(0x4016, 1); cpuram.write(0x4016, 0); //read each button out for (int i = 0; i < 8; ++i) { control = (control << 1) + (((cpuram.read(0x4016) & 3) != 0) ? 1 : 0); } //change song number if we get a button press if (((control & 0x80) != 0) && ((prevcontrol & 0x80) == 0)) { ++song; if (song > numSongs) { song = 0; } //System.err.println("next song"); init(); } else if (((control & 0x40) != 0) && ((prevcontrol & 0x40) == 0)) { --song; if (song < 0) { song = numSongs; } //System.err.println("previous song"); init(); } else //fake a jsr to the play address from wherever //unless this is a supernsf if (unfinishedcounter <= time) { cpu.push((cpu.PC - 1) >> 8); cpu.push((cpu.PC - 1) & 0xff); cpu.setPC(play); } } } @Override public String getrominfo() { return "NSF INFO: \n" + "Filename: " + loader.name + "\n" + "Size: " + loader.romlen() / 1024 + " K\n" + "Expansion Sound: " + expSound() + "\n" + "Track: " + (song + 1) + " / " + (numSongs + 1) + "\n" + "Load Address: " + utils.hex(load) + "\n" + "Init Address: " + utils.hex(init) + "\n" + "Play Address: " + utils.hex(play) + "\n" + "Banking? " + (nsfBanking ? "Yes" : "No") + "\n" + "CRC: " + utils.hex(this.crc); } private String expSound() { String chips = ""; if (vrc6) { chips += "VRC6 "; } if (vrc7) { chips += "VRC7 "; } if (n163) { chips += "Namco 163 "; } if (mmc5) { chips += "MMC5 "; } if (s5b) { chips += "Sunsoft 5B "; } if (fds) { chips += "FDS "; } return ((chips.length() > 0) ? chips : "None"); } private void setBanks() { for (int i = 0; i < prg_map.length; ++i) { prg_map[i] = (4096 * nsfBanks[i / 4]) + (1024 * (i % 4)); if ((prg_map[i]) >= prg.length) { //System.err.println("broken banks"); prg_map[i] %= prg.length; //probably a bad idea in general though //but who knows what a NSF wants when it tries //to switch a bank not in the file? //there's no "alias it to a power of 2" for nsfs since the file //size doesn't need to be a power of 2 and you can start loading //the file halfway into a bank. } } //utils.printarray(prg_map); //utils.printarray(nsfStartBanks); } private void setSoundChip() { if (((sndchip & (utils.BIT0)) != 0)) { //VRC6 audio vrc6 = true; vrc6Audio = new VRC6SoundChip(); cpuram.apu.addExpnSound(vrc6Audio); } if (((sndchip & (utils.BIT1)) != 0)) { //VRC7 audio vrc7 = true; vrc7Audio = new VRC7SoundChip(); cpuram.apu.addExpnSound(vrc7Audio); } if (((sndchip & (utils.BIT2)) != 0)) { //FDS audio, not yet implemented fds = true; fdsAudio = new FDSSoundChip(); cpuram.apu.addExpnSound(fdsAudio); } if (((sndchip & (utils.BIT3)) != 0)) { //MMC5 audio mmc5 = true; mmc5Audio = new MMC5SoundChip(); cpuram.apu.addExpnSound(mmc5Audio); } if (((sndchip & (utils.BIT4)) != 0)) { //Namco 163 audio n163 = true; n163Audio = new Namco163SoundChip(); cpuram.apu.addExpnSound(n163Audio); } if (((sndchip & (utils.BIT5)) != 0)) { //Sunsoft 5B audio s5b = true; s5bAudio = new Sunsoft5BSoundChip(); cpuram.apu.addExpnSound(s5bAudio); } } private void writeTracks() { String cur = String.format("%3d / %-3d", song + 1, numSongs + 1); for (int i = 0; i < cur.length(); ++i) { pput0[i + (32 * 28) + 6] = cur.charAt(i); } } }