package audio.gme;
// Nintendo SPC music file player
// http://www.slack.net/~ant/
/* Copyright (C) 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 SpcEmu extends SpcCpu
{
private static final class Timer
{
int time; // time of next event
int prescaler;
int period;
int divider;
int enabled;
int counter;
}
static final int ramSize = 0x10000;
static final int ramPadSize = 0x100;
// header offsets
static final int cpuStateOff = 0x25;
static final int ramOff = 0x100;
static final int dspStateOff = 0x10100;
static final int romAddr = 0xFFC0;
// SMP registers
static final int testReg = 0x0;
static final int controlReg = 0x1;
static final int dspaddrReg = 0x2;
static final int dspdataReg = 0x3;
static final int cpuio0Reg = 0x4;
static final int cpuio1Reg = 0x5;
static final int cpuio2Reg = 0x6;
static final int cpuio3Reg = 0x7;
static final int f8Reg = 0x8;
static final int f9Reg = 0x9;
static final int t0targetReg = 0xA;
static final int t1targetReg = 0xB;
static final int t2targetReg = 0xC;
static final int t0outReg = 0xD;
static final int t1outReg = 0xE;
static final int t2outReg = 0xF;
static final int romSize = 0x40;
static final int timerCount = 3;
static final int regCount = 0x10;
int dspTime;
int romEnabled;
byte [] spc;
final byte [] rom = new byte [romSize];
final byte [] hiRam = new byte [romSize];
final byte [] ram = new byte [ramSize + ramPadSize];
final int [] regs = new int [regCount];
final int [] regsIn = new int [regCount];
final SpcDsp dsp = new SpcDsp();
final Timer [] timers = new Timer [timerCount];
protected int setSampleRate_( int rate ) { return 32000; }
protected int loadFile_( byte [] in )
{
if ( !isHeader( in, "SNES-SPC700 Sound File Data" ) )
error( "Not an SPC file" );
spc = in;
// almost no SPC music rely on more than last two bytes of boot ROM
java.util.Arrays.fill( rom, 0, romSize, (byte) 0 );
rom [0x3E] = (byte) 0xFF;
rom [0x3F] = (byte) 0xC0;
// TODO: use SPC file's copy of ROM, if present?
return 1;
}
// Runs timer to present. Time must be >= t.time.
static void runTimer_( Timer t, int time )
{
int elapsed = ((time - t.time) >> t.prescaler) + 1;
t.time += elapsed << t.prescaler;
if ( t.enabled != 0 )
{
int remain = ((t.period - t.divider - 1) & 0xFF) + 1;
int divider = t.divider + elapsed;
int over;
if ( (over = elapsed - remain) >= 0 )
{
int n = over / t.period;
t.counter = (t.counter + 1 + n) & 0x0F;
divider = over - n * t.period;
}
t.divider = divider & 0xFF;
}
}
// Runs timer to present if it's not already
static void runTimer( Timer t, int time )
{
if ( time >= t.time )
runTimer_( t, time );
}
// Enables/disables boot ROM by swapping it out of RAM
private void enableRom( int enable )
{
if ( romEnabled != enable )
{
romEnabled = enable;
if ( enable != 0 )
{
System.arraycopy( ram, romAddr, hiRam, 0, romSize );
System.arraycopy( rom, 0, ram, romAddr, romSize );
}
else
{
System.arraycopy( hiRam, 0, ram, romAddr, romSize );
}
// TODO: ROM can still get overwritten when DSP writes to echo buffer
}
}
public void startTrack( int track )
{
super.startTrack( track );
time = 0;
dspTime = 32;
// RAM
java.util.Arrays.fill( ram, ramSize, ram.length, (byte) 0xFF );
System.arraycopy( spc, ramOff, ram, 0, ramSize );
dsp.init( ram, spc, dspStateOff );
// CPU
reset( ram );
pc = (spc [cpuStateOff + 1] & 0xFF) << 8 | (spc [cpuStateOff] & 0xFF);
a = spc [cpuStateOff + 2] & 0xFF;
x = spc [cpuStateOff + 3] & 0xFF;
y = spc [cpuStateOff + 4] & 0xFF;
sp = spc [cpuStateOff + 6] & 0xFF;
setPsw( spc [cpuStateOff + 5] & 0xFF );
// SMP registers
for ( int i = 0; i < regCount; i++ )
regsIn [i] = regs [i] = ram [0xF0 + i] & 0xFF;
regsIn [testReg ] = 0; // these always read back as 0
regsIn [controlReg ] = 0;
regsIn [t0targetReg] = 0;
regsIn [t1targetReg] = 0;
regsIn [t2targetReg] = 0;
// ROM
romEnabled = 0;
enableRom( regs [controlReg] & 0x80 );
// Timers
for ( int i = 0; i < timerCount; i++ )
{
Timer t = timers [i] = new Timer();
t.time = 1;
t.divider = 0;
t.period = ((regs [t0targetReg + i] - 1) & 0xFF) + 1;
t.enabled = regs [controlReg] >> i & 1;
t.counter = regsIn [t0outReg + i] & 0x0F;
}
timers [2].prescaler = 4;
timers [1].prescaler = 4 + 3;
timers [0].prescaler = 4 + 3;
// Clear echo
if ( (dsp.regs [dsp.r_flg] & 0x20) == 0 )
{
int addr = (dsp.regs [dsp.r_esa] & 0xFF) << 8;
int end = addr + ((dsp.regs [dsp.r_edl] & 0x0F) << 11);
if ( end > ramSize )
end = ramSize;
java.util.Arrays.fill( ram, addr, end, (byte) 0xFF );
}
}
protected int play_( byte out [], int count )
{
dsp.setOutput( out );
// Run for count/2*32 clocks + extra to get DSP time half-way between samples,
// since CPU might run for slightly less than requested
int clockCount = count * (32 / 2) + 16 - ((time - dspTime) & 31);
time -= clockCount;
dspTime -= clockCount;
timers [0].time -= clockCount;
timers [1].time -= clockCount;
timers [2].time -= clockCount;
runCpu();
if ( time < 0 ) // emulation error
{
logError();
return 0;
}
// Catch up to CPU
runTimer( timers [0], time );
runTimer( timers [1], time );
runTimer( timers [2], time );
// Run DSP to present
int delta;
if ( (delta = time - dspTime) >= 0 )
{
delta = (delta >> 5) + 1;
dspTime += delta << 5;
dsp.run( delta );
}
assert dsp.sampleCount() == count;
return dsp.sampleCount();
}
// Writes to SMP register
private void writeReg( int addr, int data )
{
switch ( addr )
{
case t0targetReg:
case t1targetReg:
case t2targetReg: {
Timer t = timers [addr - t0targetReg];
int period = ((data - 1) & 0xFF) + 1;
if ( t.period != period )
{
runTimer( t, time );
t.period = period;
}
break;
}
case t0outReg:
case t1outReg:
case t2outReg:
// TODO
//if ( data < no_read_before_write / 2 )
// run_timer( &m.timers [addr - t0outReg], time - 1 )->counter = 0;
break;
// Registers that act like RAM
case 0x8:
case 0x9:
regsIn [addr] = data;
break;
case testReg:
//if ( (uint8_t) data != 0x0A )
// dprintf( "SPC wrote to test register\n" );
break;
case controlReg:
// port clears
if ( (data & 0x10) != 0 )
{
regsIn [cpuio0Reg] = 0;
regsIn [cpuio1Reg] = 0;
}
if ( (data & 0x20) != 0 )
{
regsIn [cpuio2Reg] = 0;
regsIn [cpuio3Reg] = 0;
}
// timers
for ( int i = 0; i < timerCount; i++ )
{
Timer t = timers [i];
int enabled = data >> i & 1;
if ( t.enabled != enabled )
{
runTimer( t, time );
t.enabled = enabled;
if ( enabled != 0 )
{
t.divider = 0;
t.counter = 0;
}
}
}
enableRom( data & 0x80 );
break;
}
}
public final void cpuWrite( int addr, int data )
{
// RAM
ram [addr] = (byte) data;
if ( (addr -= 0xF0) >= 0 ) // 64%
{
// $F0-$FF
if ( addr < regCount ) // 87%
{
regs [addr] = (data &= 0xFF);
// Ports
// Registers other than $F2 and $F4-$F7
//if ( addr != 2 && addr != 4 && addr != 5 && addr != 6 && addr != 7 )
if ( (~0x2F000000 << addr) < 0 ) // 36%
{
if ( addr == dspdataReg ) // 99%
{
// Run DSP to present
int delta;
if ( (delta = time - dspTime) >= 0 ) // 95%
{
delta = (delta >> 5) + 1;
dspTime += delta << 5;
dsp.run( delta );
}
int dspaddr;
if ( (dspaddr = regs [dspaddrReg]) <= 0x7F )
dsp.write( dspaddr, data );
}
else
{
writeReg( addr, data );
}
}
}
// IPL ROM area or address wrapped around
else if ( (addr -= romAddr - 0xF0) >= 0 ) // 1% in IPL ROM area or address wrapped around
{
if ( addr < romSize )
{
hiRam [addr] = (byte) data;
if ( romEnabled != 0 )
ram [addr + romAddr] = rom [addr]; // restore overwritten ROM
}
else
{
if ( debug ) assert ram [addr + romAddr] == (byte) data;
ram [addr + romAddr] = (byte) 0xFF; // restore overwritten padding
cpuWrite( data, addr - (ramSize - romAddr) );
}
}
}
}
public final int cpuRead( int addr )
{
// Low RAM
if ( addr < 0xF0 ) // 60%
return ram [addr] & 0xFF;
// Timers
if ( (addr ^= 0xFF) < timerCount ) // 68%
{
Timer t = timers [2 - addr]; // TODO: reorder timers to eliminate 2-
if ( time >= t.time )
runTimer_( t, time );
int result = t.counter;
t.counter = 0;
return result;
}
// Other registers
if ( (addr ^= 0xFF) <= 0xFF ) // 9%
{
if ( addr == dspaddrReg + 0xF0 )
return regs [dspaddrReg];
if ( addr == dspdataReg + 0xF0 )
{
// DSP
// Run to present
int delta;
if ( (delta = time - dspTime) >= 0 ) // 1%
{
delta = (delta >> 5) + 1;
dspTime += delta << 5;
dsp.run( delta );
}
return dsp.regs [regs [dspaddrReg] & 0x7F] & 0xFF;
}
return regsIn [addr - 0xF0];
}
// RAM
if ( addr <= 0xFFFF ) // 99%
return ram [addr] & 0xFF;
// Address wrapped around
return cpuRead( addr - 0x10000 );
}
}