package audio.gme;
// Nintendo Game Boy GBS 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 GbsEmu extends GbCpu
{
// header offsets
static final int trackCountOff = 0x04;
static final int loadAddrOff = 0x06;
static final int initAddrOff = 0x08;
static final int playAddrOff = 0x0A;
static final int stackPtrOff = 0x0C;
static final int timerModuloOff = 0x0E;
static final int timerModeOff = 0x0F;
// memory addresses
static final int idleAddr = 0xF00D;
static final int ramAddr = 0xA000;
static final int hiPage = 0xFF00 - ramAddr;
static final int ramSize = 0x4000 + 0x2000;
static final int bankSize = 0x4000;
final MemPager rom = new MemPager( bankSize, ramSize );
final byte [] header = new byte [0x70];
byte [] ram;
int endTime;
int playPeriod;
int nextPlay;
GbApu apu = new GbApu();
protected int loadFile_( byte in [] )
{
if ( !isHeader( in, "GBS\u0001" ) )
error( "Not a GBS file" );
rstBase = getLE16( in, loadAddrOff );
ram = rom.load( in, header, rstBase, 0xFF );
setClockRate( 4194304 );
apu.setOutput( buf.center(), buf.left(), buf.right() );
return header [trackCountOff] & 0xFF;
}
final void setBank( int n )
{
int addr = rom.maskAddr( n * bankSize );
if ( addr == 0 && rom.size() > bankSize )
n = 1;
mapMemory( bankSize, bankSize, rom.mapAddr( addr ) );
}
static final byte [] rates = { 10, 4, 6, 8 };
void updateTimer()
{
playPeriod = 70224; // 59.73 Hz
if ( (header [timerModeOff] & 0x04) != 0 )
{
int shift = rates [ram [hiPage + 7] & 3] - (header [timerModeOff] >> 7 & 1);
playPeriod = (256 - (ram [hiPage + 6] & 0xFF)) << shift;
}
}
static final int [] sound_data = {
0x80, 0xBF, 0x00, 0x00, 0xBF, // square 1
0x00, 0x3F, 0x00, 0x00, 0xBF, // square 2
0x7F, 0xFF, 0x9F, 0x00, 0xBF, // wave
0x00, 0xFF, 0x00, 0x00, 0xBF, // noise
0x77, 0xFF, 0x80, // vin/volume, status, power mode
0, 0, 0, 0, 0, 0, 0, 0, 0, // unused
0xAC, 0xDD, 0xDA, 0x48, 0x36, 0x02, 0xCF, 0x16, // waveform data
0x2C, 0x04, 0xE5, 0x2C, 0xAC, 0xDD, 0xDA, 0x48
};
void cpuCall( int addr )
{
assert sp == getLE16( header, stackPtrOff );
pc = addr;
cpuWrite( --sp, idleAddr >> 8 );
cpuWrite( --sp, idleAddr&0xFF );
}
public void startTrack( int track )
{
super.startTrack( track );
apu.reset();
apu.write( 0, 0xFF26, 0x80 ); // power on
for ( int i = 0; i < sound_data.length; i++ )
apu.write( 0, i + apu.startAddr, sound_data [i] );
reset( ram, rom.unmapped() );
mapMemory( ramAddr, 0x10000 - ramAddr, 0 );
mapMemory( 0, bankSize, rom.mapAddr( 0 ) );
setBank( 1 );
java.util.Arrays.fill( ram, 0, 0x4000, (byte) 0 );
java.util.Arrays.fill( ram, 0x4000, 0x5F80, (byte) 0xFF );
java.util.Arrays.fill( ram, 0x5F80, ramSize, (byte) 0 );
ram [hiPage] = 0; // joypad reads back as 0
ram [mapAddr( idleAddr )] = (byte) 0xED; // illegal instruction
ram [hiPage + 6] = header [timerModuloOff];
ram [hiPage + 7] = header [timerModeOff];
updateTimer();
nextPlay = playPeriod;
a = track;
pc = idleAddr;
sp = getLE16( header, stackPtrOff );
cpuCall( getLE16( header, initAddrOff ) );
}
protected int runClocks( int clockCount )
{
endTime = clockCount;
time = -endTime;
while ( true )
{
runCpu();
if ( time >= 0 )
break;
if ( pc != idleAddr )
{
// TODO: PC overflow handling
pc = (pc + 1) & 0xFFFF;
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 int cpuRead( int addr )
{
if ( debug ) assert 0 <= addr && addr < 0x10000;
if ( apu.startAddr <= addr && addr <= apu.endAddr )
return apu.read( time + endTime, addr );
return ram [mapAddr( addr )] & 0xFF;
}
protected void cpuWrite( int addr, int data )
{
if ( debug ) assert 0 <= data && data < 0x100;
if ( debug ) assert 0 <= addr && addr < 0x10000;
int offset = addr - ramAddr;
if ( offset >= 0 )
{
ram [offset] = (byte) data;
if ( addr < 0xFF80 && addr >= 0xFF00 )
{
if ( apu.startAddr <= addr && addr <= apu.endAddr )
{
apu.write( time + endTime, addr, data );
}
else if ( (addr ^ 0xFF06) < 2 )
{
updateTimer();
}
else if ( addr == 0xFF00 )
{
ram [offset] = 0; // keep joypad return value 0
}
else
{
ram [offset] = (byte) 0xFF;
}
}
}
else if ( (addr ^ 0x2000) <= 0x2000 - 1 )
{
setBank( data );
}
}
}