package audio.gme; // Nintendo NSF music file emulator // http://www.slack.net/~ant/ /* Copyright (C) 2003-2007 Shay Green. This module is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this module; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ final class NsfEmu extends NesCpu { // header offsets static final int trackCountOff = 0x06; static final int loadAddrOff = 0x08; static final int initAddrOff = 0x0A; static final int playAddrOff = 0x0C; static final int ntscSpeedOff = 0x6E; static final int banksOff = 0x70; static final int palSpeedOff = 0x78; static final int speedFlagsOff = 0x7A; static final int chipFlagsOff = 0x7B; // memory addresses static final int sramAddr = 0x6000; static final int bankSelectAddr = 0x5FF8; static final int idleAddr = bankSelectAddr; static final int romStart = 0x8000; static final int sramOffset = 0x800; // offset in ram [] static final int sramSize = 0x2000; static final int unmapped4000Offset = sramAddr + sramSize; static final int ramSize = unmapped4000Offset + 0x100; static final int bankSize = 0x1000; static final int bankCount = 8; byte [] ram; final MemPager rom = new MemPager( bankSize, ramSize ); final byte [] header = new byte [0x80]; final int [] initialBanks = new int [8]; final NesApu apu = new NesApu(); int palOnly; int endTime; int playPeriod; int nextPlay; protected int loadFile_( byte [] in ) { if ( !isHeader( in, "NESM" ) ) error( "Not an NSF file" ); // Load ROM data final int loadAddr = getLE16( in, loadAddrOff ); ram = rom.load( in, header, loadAddr % bankSize, 0xF2 ); if ( header [chipFlagsOff] != 0 ) error( "Extra sound chips not supported" ); // Copy initial banks int nonZero = 0; for ( int i = 0; i < bankCount; i++ ) { int bank = header [banksOff + i] & 0xFF; initialBanks [i] = bank; nonZero |= bank; } // Use default banks if initial banks were all zero if ( nonZero == 0 ) { int totalBanks = rom.size() / bankSize; int firstBank = (loadAddr - romStart) / bankSize; for ( int i = 0; i < bankCount; i++ ) { int bank = i - firstBank; if ( bank < 0 || totalBanks <= bank ) bank = 0; initialBanks [i] = bank; } } // NTSC rate int playbackRate = getLE16( header, ntscSpeedOff ); double clockRate = 1789772.727273; int standardRate = 0x411A; playPeriod = 29781; palOnly = 0; if ( (header [speedFlagsOff] & 3) == 1 ) { // PAL rate playbackRate = getLE16( header, palSpeedOff ); clockRate = 1662607.125; standardRate = 0x4E20; playPeriod = 33247; palOnly = 1; } // Custom rate if ( playbackRate != standardRate && playbackRate != 0 ) playPeriod = (int) (playbackRate * clockRate * (1.0 / 1000000.0) + 0.5); setClockRate( (int) (clockRate + 0.5) ); apu.setOutput( buf.center() ); return header [trackCountOff] & 0xFF; } private void cpuCall( int addr ) { pc = addr; p |= 0x04; ram [ s | 0x100] = (byte) ((idleAddr - 1) >> 8); ram [(s + 0xFF) | 0x100] = (byte) (idleAddr - 1); s = (s - 2) & 0xFF; } public void startTrack( int track ) { super.startTrack( track ); // APU apu.reset( this, (palOnly != 0) ); apu.write( 0, 0x4015, 0x0F ); // Memory java.util.Arrays.fill( ram, 0, ramSize, (byte) 0 ); reset( ram, rom.unmapped() ); mapMemory( 0, sramOffset, 0 ); mapMemory( sramAddr, sramSize, sramOffset ); // some NSF rips expect to read back 0 from 0x4016 and 0x4017 (Maniac Mansion) mapMemory( 0x4000, pageSize, unmapped4000Offset ); for ( int i = 0; i < bankCount; ++i ) cpuWrite( bankSelectAddr + i, initialBanks [i] ); nextPlay = playPeriod; // CPU a = track; x = palOnly; y = 0; p = 0; s = 0xFF; pc = idleAddr; cpuCall( getLE16( header, initAddrOff ) ); } protected int runClocks( int clockCount ) { endTime = clockCount; time = -endTime; while ( true ) { runCpu(); if ( time >= 0 ) break; if ( pc != idleAddr ) { logError(); return endTime; } // Next play call int next = nextPlay - endTime; if ( time < next ) { time = 0; if ( next > 0 ) break; time = next; } nextPlay += playPeriod; cpuCall( getLE16( header, playAddrOff ) ); } // End time frame endTime += time; nextPlay -= endTime; if ( nextPlay < 0 ) // could go negative if routine is taking too long to return nextPlay = 0; apu.endFrame( endTime ); return endTime; } protected final int cpuRead( int addr ) { if ( addr <= 0x7FF ) // 90% return ram [addr] & 0xFF; // APU if ( addr == 0x4015 ) return apu.read( time + endTime ); // TODO: return addr >> 8 for unmapped areas? if ( addr < 0x10000 ) return ram [mapAddr( addr )] & 0xFF; // address wrapped around return ram [addr - 0x10000] & 0xFF; } protected final void cpuWrite( int addr, int data ) { if ( debug ) assert 0 <= data && data < 0x100; if ( debug ) assert 0 <= addr && addr < 0x10100; // SRAM int offset; if ( (offset = addr ^ sramAddr) < sramSize ) { ram [sramOffset + offset] = (byte) data; return; } // APU if ( (addr ^ 0x4000) <= 0x17 ) { apu.write( time + endTime, addr, data ); return; } // Bank int bank = addr - bankSelectAddr; if ( 0 <= bank && bank < bankCount ) { mapMemory( bank * bankSize + romStart, bankSize, rom.mapAddr( data * bankSize ) ); return; } // RAM if ( (addr & 0xF800) == 0 ) // addr <= 0x7FF || addr >= 0x10000 { ram [addr & 0x7FF] = (byte) data; return; } } }