mirror of
https://github.com/DrBeef/Raze.git
synced 2024-12-15 23:21:21 +00:00
718112a8fe
Currently none of these is being used, but eventually they will, once more code gets ported over. So it's better to have them right away and avoid editing the project file too much, only to revert that later.
531 lines
12 KiB
C++
531 lines
12 KiB
C++
// Game_Music_Emu https://bitbucket.org/mpyne/game-music-emu/
|
|
|
|
#include "Hes_Emu.h"
|
|
|
|
#include "blargg_endian.h"
|
|
#include <string.h>
|
|
|
|
/* Copyright (C) 2006 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 */
|
|
|
|
#include "blargg_source.h"
|
|
|
|
int const timer_mask = 0x04;
|
|
int const vdp_mask = 0x02;
|
|
int const i_flag_mask = 0x04;
|
|
int const unmapped = 0xFF;
|
|
|
|
long const period_60hz = 262 * 455L; // scanlines * clocks per scanline
|
|
|
|
Hes_Emu::Hes_Emu()
|
|
{
|
|
timer.raw_load = 0;
|
|
set_type( gme_hes_type );
|
|
|
|
static const char* const names [Hes_Apu::osc_count] = {
|
|
"Wave 1", "Wave 2", "Wave 3", "Wave 4", "Multi 1", "Multi 2"
|
|
};
|
|
set_voice_names( names );
|
|
|
|
static int const types [Hes_Apu::osc_count] = {
|
|
wave_type | 0, wave_type | 1, wave_type | 2, wave_type | 3,
|
|
mixed_type | 0, mixed_type | 1
|
|
};
|
|
set_voice_types( types );
|
|
set_silence_lookahead( 6 );
|
|
set_gain( 1.11 );
|
|
}
|
|
|
|
Hes_Emu::~Hes_Emu() { }
|
|
|
|
void Hes_Emu::unload()
|
|
{
|
|
rom.clear();
|
|
Music_Emu::unload();
|
|
}
|
|
|
|
// Track info
|
|
|
|
static byte const* copy_field( byte const* in, char* out )
|
|
{
|
|
if ( in )
|
|
{
|
|
int len = 0x20;
|
|
if ( in [0x1F] && !in [0x2F] )
|
|
len = 0x30; // fields are sometimes 16 bytes longer (ugh)
|
|
|
|
// since text fields are where any data could be, detect non-text
|
|
// and fields with data after zero byte terminator
|
|
|
|
int i = 0;
|
|
for ( i = 0; i < len && in [i]; i++ )
|
|
if ( ((in [i] + 1) & 0xFF) < ' ' + 1 ) // also treat 0xFF as non-text
|
|
return 0; // non-ASCII found
|
|
|
|
for ( ; i < len; i++ )
|
|
if ( in [i] )
|
|
return 0; // data after terminator
|
|
|
|
Gme_File::copy_field_( out, (char const*) in, len );
|
|
in += len;
|
|
}
|
|
return in;
|
|
}
|
|
|
|
static void copy_hes_fields( byte const* in, track_info_t* out )
|
|
{
|
|
if ( *in >= ' ' )
|
|
{
|
|
in = copy_field( in, out->game );
|
|
in = copy_field( in, out->author );
|
|
in = copy_field( in, out->copyright );
|
|
}
|
|
}
|
|
|
|
blargg_err_t Hes_Emu::track_info_( track_info_t* out, int ) const
|
|
{
|
|
copy_hes_fields( rom.begin() + 0x20, out );
|
|
return 0;
|
|
}
|
|
|
|
static blargg_err_t check_hes_header( void const* header )
|
|
{
|
|
if ( memcmp( header, "HESM", 4 ) )
|
|
return gme_wrong_file_type;
|
|
return 0;
|
|
}
|
|
|
|
struct Hes_File : Gme_Info_
|
|
{
|
|
struct header_t {
|
|
char header [Hes_Emu::header_size];
|
|
char unused [0x20];
|
|
byte fields [0x30 * 3];
|
|
} h;
|
|
|
|
Hes_File() { set_type( gme_hes_type ); }
|
|
|
|
blargg_err_t load_( Data_Reader& in )
|
|
{
|
|
assert( offsetof (header_t,fields) == Hes_Emu::header_size + 0x20 );
|
|
blargg_err_t err = in.read( &h, sizeof h );
|
|
if ( err )
|
|
return (err == in.eof_error ? gme_wrong_file_type : err);
|
|
return check_hes_header( &h );
|
|
}
|
|
|
|
blargg_err_t track_info_( track_info_t* out, int ) const
|
|
{
|
|
copy_hes_fields( h.fields, out );
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
static Music_Emu* new_hes_emu () { return BLARGG_NEW Hes_Emu ; }
|
|
static Music_Emu* new_hes_file() { return BLARGG_NEW Hes_File; }
|
|
|
|
static gme_type_t_ const gme_hes_type_ = { "PC Engine", 256, &new_hes_emu, &new_hes_file, "HES", 1 };
|
|
BLARGG_EXPORT extern gme_type_t const gme_hes_type = &gme_hes_type_;
|
|
|
|
|
|
// Setup
|
|
|
|
blargg_err_t Hes_Emu::load_( Data_Reader& in )
|
|
{
|
|
assert( offsetof (header_t,unused [4]) == header_size );
|
|
RETURN_ERR( rom.load( in, header_size, &header_, unmapped ) );
|
|
|
|
RETURN_ERR( check_hes_header( header_.tag ) );
|
|
|
|
if ( header_.vers != 0 )
|
|
set_warning( "Unknown file version" );
|
|
|
|
if ( memcmp( header_.data_tag, "DATA", 4 ) )
|
|
set_warning( "Data header missing" );
|
|
|
|
if ( memcmp( header_.unused, "\0\0\0\0", 4 ) )
|
|
set_warning( "Unknown header data" );
|
|
|
|
// File spec supports multiple blocks, but I haven't found any, and
|
|
// many files have bad sizes in the only block, so it's simpler to
|
|
// just try to load the damn data as best as possible.
|
|
|
|
long addr = get_le32( header_.addr );
|
|
long size = get_le32( header_.size );
|
|
long const rom_max = 0x100000;
|
|
if ( addr & ~(rom_max - 1) )
|
|
{
|
|
set_warning( "Invalid address" );
|
|
addr &= rom_max - 1;
|
|
}
|
|
if ( (unsigned long) (addr + size) > (unsigned long) rom_max )
|
|
set_warning( "Invalid size" );
|
|
|
|
if ( size != rom.file_size() )
|
|
{
|
|
if ( size <= rom.file_size() - 4 && !memcmp( rom.begin() + size, "DATA", 4 ) )
|
|
set_warning( "Multiple DATA not supported" );
|
|
else if ( size < rom.file_size() )
|
|
set_warning( "Extra file data" );
|
|
else
|
|
set_warning( "Missing file data" );
|
|
}
|
|
|
|
rom.set_addr( addr );
|
|
|
|
set_voice_count( apu.osc_count );
|
|
|
|
apu.volume( gain() );
|
|
|
|
return setup_buffer( 7159091 );
|
|
}
|
|
|
|
void Hes_Emu::update_eq( blip_eq_t const& eq )
|
|
{
|
|
apu.treble_eq( eq );
|
|
}
|
|
|
|
void Hes_Emu::set_voice( int i, Blip_Buffer* center, Blip_Buffer* left, Blip_Buffer* right )
|
|
{
|
|
apu.osc_output( i, center, left, right );
|
|
}
|
|
|
|
// Emulation
|
|
|
|
void Hes_Emu::recalc_timer_load()
|
|
{
|
|
timer.load = timer.raw_load * timer_base + 1;
|
|
}
|
|
|
|
void Hes_Emu::set_tempo_( double t )
|
|
{
|
|
play_period = hes_time_t (period_60hz / t);
|
|
timer_base = int (1024 / t);
|
|
recalc_timer_load();
|
|
}
|
|
|
|
blargg_err_t Hes_Emu::start_track_( int track )
|
|
{
|
|
RETURN_ERR( Classic_Emu::start_track_( track ) );
|
|
|
|
memset( ram, 0, sizeof ram ); // some HES music relies on zero fill
|
|
memset( sgx, 0, sizeof sgx );
|
|
|
|
apu.reset();
|
|
cpu::reset();
|
|
|
|
for ( unsigned i = 0; i < sizeof header_.banks; i++ )
|
|
set_mmr( i, header_.banks [i] );
|
|
set_mmr( page_count, 0xFF ); // unmapped beyond end of address space
|
|
|
|
irq.disables = timer_mask | vdp_mask;
|
|
irq.timer = future_hes_time;
|
|
irq.vdp = future_hes_time;
|
|
|
|
timer.enabled = false;
|
|
timer.raw_load= 0x80;
|
|
timer.count = timer.load;
|
|
timer.fired = false;
|
|
timer.last_time = 0;
|
|
|
|
vdp.latch = 0;
|
|
vdp.control = 0;
|
|
vdp.next_vbl = 0;
|
|
|
|
ram [0x1FF] = (idle_addr - 1) >> 8;
|
|
ram [0x1FE] = (idle_addr - 1) & 0xFF;
|
|
r.sp = 0xFD;
|
|
r.pc = get_le16( header_.init_addr );
|
|
r.a = track;
|
|
|
|
recalc_timer_load();
|
|
last_frame_hook = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Hardware
|
|
|
|
void Hes_Emu::cpu_write_vdp( int addr, int data )
|
|
{
|
|
switch ( addr )
|
|
{
|
|
case 0:
|
|
vdp.latch = data & 0x1F;
|
|
break;
|
|
|
|
case 2:
|
|
if ( vdp.latch == 5 )
|
|
{
|
|
if ( data & 0x04 )
|
|
set_warning( "Scanline interrupt unsupported" );
|
|
run_until( time() );
|
|
vdp.control = data;
|
|
irq_changed();
|
|
}
|
|
else
|
|
{
|
|
debug_printf( "VDP not supported: $%02X <- $%02X\n", vdp.latch, data );
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
debug_printf( "VDP MSB not supported: $%02X <- $%02X\n", vdp.latch, data );
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Hes_Emu::cpu_write_( hes_addr_t addr, int data )
|
|
{
|
|
if ( unsigned (addr - apu.start_addr) <= apu.end_addr - apu.start_addr )
|
|
{
|
|
GME_APU_HOOK( this, addr - apu.start_addr, data );
|
|
// avoid going way past end when a long block xfer is writing to I/O space
|
|
hes_time_t t = min( time(), end_time() + 8 );
|
|
apu.write_data( t, addr, data );
|
|
return;
|
|
}
|
|
|
|
hes_time_t time = this->time();
|
|
switch ( addr )
|
|
{
|
|
case 0x0000:
|
|
case 0x0002:
|
|
case 0x0003:
|
|
cpu_write_vdp( addr, data );
|
|
return;
|
|
|
|
case 0x0C00: {
|
|
run_until( time );
|
|
timer.raw_load = (data & 0x7F) + 1;
|
|
recalc_timer_load();
|
|
timer.count = timer.load;
|
|
break;
|
|
}
|
|
|
|
case 0x0C01:
|
|
data &= 1;
|
|
if ( timer.enabled == data )
|
|
return;
|
|
run_until( time );
|
|
timer.enabled = data;
|
|
if ( data )
|
|
timer.count = timer.load;
|
|
break;
|
|
|
|
case 0x1402:
|
|
run_until( time );
|
|
irq.disables = data;
|
|
if ( (data & 0xF8) && (data & 0xF8) != 0xF8 ) // flag questionable values
|
|
debug_printf( "Int mask: $%02X\n", data );
|
|
break;
|
|
|
|
case 0x1403:
|
|
run_until( time );
|
|
if ( timer.enabled )
|
|
timer.count = timer.load;
|
|
timer.fired = false;
|
|
break;
|
|
|
|
#ifndef NDEBUG
|
|
case 0x1000: // I/O port
|
|
case 0x0402: // palette
|
|
case 0x0403:
|
|
case 0x0404:
|
|
case 0x0405:
|
|
return;
|
|
|
|
default:
|
|
debug_printf( "unmapped write $%04X <- $%02X\n", addr, data );
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
irq_changed();
|
|
}
|
|
|
|
int Hes_Emu::cpu_read_( hes_addr_t addr )
|
|
{
|
|
hes_time_t time = this->time();
|
|
addr &= page_size - 1;
|
|
switch ( addr )
|
|
{
|
|
case 0x0000:
|
|
if ( irq.vdp > time )
|
|
return 0;
|
|
irq.vdp = future_hes_time;
|
|
run_until( time );
|
|
irq_changed();
|
|
return 0x20;
|
|
|
|
case 0x0002:
|
|
case 0x0003:
|
|
debug_printf( "VDP read not supported: %d\n", addr );
|
|
return 0;
|
|
|
|
case 0x0C01:
|
|
//return timer.enabled; // TODO: remove?
|
|
case 0x0C00:
|
|
run_until( time );
|
|
debug_printf( "Timer count read\n" );
|
|
return (unsigned) (timer.count - 1) / timer_base;
|
|
|
|
case 0x1402:
|
|
return irq.disables;
|
|
|
|
case 0x1403:
|
|
{
|
|
int status = 0;
|
|
if ( irq.timer <= time ) status |= timer_mask;
|
|
if ( irq.vdp <= time ) status |= vdp_mask;
|
|
return status;
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
case 0x1000: // I/O port
|
|
case 0x180C: // CD-ROM
|
|
case 0x180D:
|
|
break;
|
|
|
|
default:
|
|
debug_printf( "unmapped read $%04X\n", addr );
|
|
#endif
|
|
}
|
|
|
|
return unmapped;
|
|
}
|
|
|
|
// see hes_cpu_io.h for core read/write functions
|
|
|
|
// Emulation
|
|
|
|
void Hes_Emu::run_until( hes_time_t present )
|
|
{
|
|
while ( vdp.next_vbl < present )
|
|
vdp.next_vbl += play_period;
|
|
|
|
hes_time_t elapsed = present - timer.last_time;
|
|
if ( elapsed > 0 )
|
|
{
|
|
if ( timer.enabled )
|
|
{
|
|
timer.count -= elapsed;
|
|
if ( timer.count <= 0 )
|
|
timer.count += timer.load;
|
|
}
|
|
timer.last_time = present;
|
|
}
|
|
}
|
|
|
|
void Hes_Emu::irq_changed()
|
|
{
|
|
hes_time_t present = time();
|
|
|
|
if ( irq.timer > present )
|
|
{
|
|
irq.timer = future_hes_time;
|
|
if ( timer.enabled && !timer.fired )
|
|
irq.timer = present + timer.count;
|
|
}
|
|
|
|
if ( irq.vdp > present )
|
|
{
|
|
irq.vdp = future_hes_time;
|
|
if ( vdp.control & 0x08 )
|
|
irq.vdp = vdp.next_vbl;
|
|
}
|
|
|
|
hes_time_t time = future_hes_time;
|
|
if ( !(irq.disables & timer_mask) ) time = irq.timer;
|
|
if ( !(irq.disables & vdp_mask) ) time = min( time, irq.vdp );
|
|
|
|
set_irq_time( time );
|
|
}
|
|
|
|
int Hes_Emu::cpu_done()
|
|
{
|
|
check( time() >= end_time() ||
|
|
(!(r.status & i_flag_mask) && time() >= irq_time()) );
|
|
|
|
if ( !(r.status & i_flag_mask) )
|
|
{
|
|
hes_time_t present = time();
|
|
|
|
if ( irq.timer <= present && !(irq.disables & timer_mask) )
|
|
{
|
|
timer.fired = true;
|
|
irq.timer = future_hes_time;
|
|
irq_changed(); // overkill, but not worth writing custom code
|
|
#if GME_FRAME_HOOK_DEFINED
|
|
{
|
|
unsigned const threshold = period_60hz / 30;
|
|
unsigned long elapsed = present - last_frame_hook;
|
|
if ( elapsed - period_60hz + threshold / 2 < threshold )
|
|
{
|
|
last_frame_hook = present;
|
|
GME_FRAME_HOOK( this );
|
|
}
|
|
}
|
|
#endif
|
|
return 0x0A;
|
|
}
|
|
|
|
if ( irq.vdp <= present && !(irq.disables & vdp_mask) )
|
|
{
|
|
// work around for bugs with music not acknowledging VDP
|
|
//run_until( present );
|
|
//irq.vdp = future_hes_time;
|
|
//irq_changed();
|
|
#if GME_FRAME_HOOK_DEFINED
|
|
last_frame_hook = present;
|
|
GME_FRAME_HOOK( this );
|
|
#endif
|
|
return 0x08;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void adjust_time( blargg_long& time, hes_time_t delta )
|
|
{
|
|
if ( time < future_hes_time )
|
|
{
|
|
time -= delta;
|
|
if ( time < 0 )
|
|
time = 0;
|
|
}
|
|
}
|
|
|
|
blargg_err_t Hes_Emu::run_clocks( blip_time_t& duration_, int )
|
|
{
|
|
blip_time_t const duration = duration_; // cache
|
|
|
|
if ( cpu::run( duration ) )
|
|
set_warning( "Emulation error (illegal instruction)" );
|
|
|
|
check( time() >= duration );
|
|
//check( time() - duration < 20 ); // Txx instruction could cause going way over
|
|
|
|
run_until( duration );
|
|
|
|
// end time frame
|
|
timer.last_time -= duration;
|
|
vdp.next_vbl -= duration;
|
|
#if GME_FRAME_HOOK_DEFINED
|
|
last_frame_hook -= duration;
|
|
#endif
|
|
cpu::end_frame( duration );
|
|
::adjust_time( irq.timer, duration );
|
|
::adjust_time( irq.vdp, duration );
|
|
apu.end_frame( duration );
|
|
|
|
return 0;
|
|
}
|