// Game_Music_Emu $vers. http://www.slack.net/~ant/ #include "Gym_Emu.h" #include "blargg_endian.h" /* Copyright (C) 2003-2008 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" double const min_tempo = 0.25; double const oversample = 5 / 3.0; double const fm_gain = 3.0; int const base_clock = 53700300; int const clock_rate = base_clock / 15; Gym_Emu::Gym_Emu() { resampler.set_callback( play_frame_, this ); pos = NULL; disable_oversampling_ = false; set_type( gme_gym_type ); set_silence_lookahead( 1 ); // tracks should already be trimmed pcm_buf = stereo_buf.center(); } Gym_Emu::~Gym_Emu() { } // Track info static void get_gym_info( Gym_Emu::header_t const& h, int length, track_info_t* out ) { if ( 0 != memcmp( h.tag, "GYMX", 4 ) ) return; length = length * 50 / 3; // 1000 / 60 int loop = get_le32( h.loop_start ); if ( loop ) { out->intro_length = loop * 50 / 3; out->loop_length = length - out->intro_length; } else { out->length = length; out->intro_length = length; // make it clear that track is no longer than length out->loop_length = 0; } // more stupidity where the field should have been left blank if ( strcmp( h.song, "Unknown Song" ) ) GME_COPY_FIELD( h, out, song ); if ( strcmp( h.game, "Unknown Game" ) ) GME_COPY_FIELD( h, out, game ); if ( strcmp( h.copyright, "Unknown Publisher" ) ) GME_COPY_FIELD( h, out, copyright ); if ( strcmp( h.dumper, "Unknown Person" ) ) GME_COPY_FIELD( h, out, dumper ); if ( strcmp( h.comment, "Header added by YMAMP" ) ) GME_COPY_FIELD( h, out, comment ); } static void hash_gym_file( Gym_Emu::header_t const& h, byte const* data, int data_size, Music_Emu::Hash_Function& out ) { out.hash_( &h.loop_start[0], sizeof(h.loop_start) ); out.hash_( &h.packed[0], sizeof(h.packed) ); out.hash_( data, data_size ); } static int gym_track_length( byte const p [], byte const* end ) { int time = 0; while ( p < end ) { switch ( *p++ ) { case 0: time++; break; case 1: case 2: p += 2; break; case 3: p += 1; break; } } return time; } blargg_err_t Gym_Emu::track_info_( track_info_t* out, int ) const { get_gym_info( header_, gym_track_length( log_begin(), file_end() ), out ); return blargg_ok; } static blargg_err_t check_header( byte const in [], int size, int* data_offset = NULL ) { if ( size < 4 ) return blargg_err_file_type; if ( memcmp( in, "GYMX", 4 ) == 0 ) { if ( size < Gym_Emu::header_t::size + 1 ) return blargg_err_file_type; if ( memcmp( ((Gym_Emu::header_t const*) in)->packed, "\0\0\0\0", 4 ) != 0 ) return BLARGG_ERR( BLARGG_ERR_FILE_FEATURE, "packed GYM file" ); if ( data_offset ) *data_offset = Gym_Emu::header_t::size; } else if ( *in > 3 ) { return blargg_err_file_type; } return blargg_ok; } struct Gym_File : Gme_Info_ { int data_offset; Gym_File() { set_type( gme_gym_type ); } blargg_err_t load_mem_( byte const in [], int size ) { data_offset = 0; return check_header( in, size, &data_offset ); } blargg_err_t track_info_( track_info_t* out, int ) const { int length = gym_track_length( &file_begin() [data_offset], file_end() ); get_gym_info( *(Gym_Emu::header_t const*) file_begin(), length, out ); return blargg_ok; } blargg_err_t hash_( Hash_Function& out ) const { Gym_Emu::header_t const* h = ( Gym_Emu::header_t const* ) file_begin(); byte const* data = &file_begin() [data_offset]; hash_gym_file( *h, data, file_end() - data, out ); return blargg_ok; } }; static Music_Emu* new_gym_emu () { return BLARGG_NEW Gym_Emu ; } static Music_Emu* new_gym_file() { return BLARGG_NEW Gym_File; } gme_type_t_ const gme_gym_type [1] = {{ "Sega Genesis", 1, &new_gym_emu, &new_gym_file, "GYM", 0 }}; // Setup blargg_err_t Gym_Emu::set_sample_rate_( int sample_rate ) { blip_eq_t eq( -32, 8000, sample_rate ); apu.treble_eq( eq ); pcm_synth.treble_eq( eq ); apu.volume( 0.135 * fm_gain * gain() ); double factor = oversample; if ( disable_oversampling_ ) factor = (double) base_clock / 7 / 144 / sample_rate; RETURN_ERR( resampler.setup( factor, 0.990, fm_gain * gain() ) ); factor = resampler.rate(); double fm_rate = sample_rate * factor; RETURN_ERR( stereo_buf.set_sample_rate( sample_rate, int (1000 / 60.0 / min_tempo) ) ); stereo_buf.clock_rate( clock_rate ); RETURN_ERR( fm.set_rate( fm_rate, base_clock / 7.0 ) ); RETURN_ERR( resampler.reset( (int) (1.0 / 60 / min_tempo * sample_rate) ) ); return blargg_ok; } void Gym_Emu::set_tempo_( double t ) { if ( t < min_tempo ) { set_tempo( min_tempo ); return; } if ( stereo_buf.sample_rate() ) { double denom = tempo() * 60; clocks_per_frame = (int) (clock_rate / denom); resampler.resize( (int) (sample_rate() / denom) ); } } void Gym_Emu::mute_voices_( int mask ) { Music_Emu::mute_voices_( mask ); fm.mute_voices( mask ); apu.set_output( (mask & 0x80) ? 0 : stereo_buf.center() ); pcm_synth.volume( (mask & 0x40) ? 0.0 : 0.125 / 256 * fm_gain * gain() ); } blargg_err_t Gym_Emu::load_mem_( byte const in [], int size ) { assert( offsetof (header_t,packed [4]) == header_t::size ); log_offset = 0; RETURN_ERR( check_header( in, size, &log_offset ) ); loop_begin = NULL; static const char* const names [] = { "FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "PCM", "PSG" }; set_voice_names( names ); set_voice_count( 8 ); if ( log_offset ) header_ = *(header_t const*) in; else memset( &header_, 0, sizeof header_ ); return blargg_ok; } // Emulation blargg_err_t Gym_Emu::start_track_( int track ) { RETURN_ERR( Music_Emu::start_track_( track ) ); pos = log_begin(); loop_remain = get_le32( header_.loop_start ); prev_pcm_count = 0; pcm_enabled = 0; pcm_amp = -1; fm.reset(); apu.reset(); stereo_buf.clear(); resampler.clear(); pcm_buf = stereo_buf.center(); return blargg_ok; } void Gym_Emu::run_pcm( byte const pcm_in [], int pcm_count ) { // Guess beginning and end of sample and adjust rate and buffer position accordingly. // count dac samples in next frame int next_pcm_count = 0; const byte* p = this->pos; int cmd; while ( (cmd = *p++) != 0 ) { int data = *p++; if ( cmd <= 2 ) ++p; if ( cmd == 1 && data == 0x2A ) next_pcm_count++; } // detect beginning and end of sample int rate_count = pcm_count; int start = 0; if ( !prev_pcm_count && next_pcm_count && pcm_count < next_pcm_count ) { rate_count = next_pcm_count; start = next_pcm_count - pcm_count; } else if ( prev_pcm_count && !next_pcm_count && pcm_count < prev_pcm_count ) { rate_count = prev_pcm_count; } // Evenly space samples within buffer section being used blip_resampled_time_t period = pcm_buf->resampled_duration( clocks_per_frame ) / rate_count; blip_resampled_time_t time = pcm_buf->resampled_time( 0 ) + period * start + (unsigned) period / 2; int pcm_amp = this->pcm_amp; if ( pcm_amp < 0 ) pcm_amp = pcm_in [0]; for ( int i = 0; i < pcm_count; i++ ) { int delta = pcm_in [i] - pcm_amp; pcm_amp += delta; pcm_synth.offset_resampled( time, delta, pcm_buf ); time += period; } this->pcm_amp = pcm_amp; pcm_buf->set_modified(); } void Gym_Emu::parse_frame() { byte pcm [1024]; // all PCM writes for frame int pcm_size = 0; const byte* pos = this->pos; if ( loop_remain && !--loop_remain ) loop_begin = pos; // find loop on first time through sequence int cmd; while ( (cmd = *pos++) != 0 ) { int data = *pos++; if ( cmd == 1 ) { int data2 = *pos++; if ( data == 0x2A ) { pcm [pcm_size] = data2; if ( pcm_size < (int) sizeof pcm - 1 ) pcm_size += pcm_enabled; } else { if ( data == 0x2B ) pcm_enabled = data2 >> 7 & 1; fm.write0( data, data2 ); } } else if ( cmd == 2 ) { int data2 = *pos++; if ( data == 0xB6 ) { Blip_Buffer * pcm_buf = NULL; switch ( data2 >> 6 ) { case 0: pcm_buf = NULL; break; case 1: pcm_buf = stereo_buf.right(); break; case 2: pcm_buf = stereo_buf.left(); break; case 3: pcm_buf = stereo_buf.center(); break; } /*if ( this->pcm_buf != pcm_buf ) { if ( this->pcm_buf ) pcm_synth.offset_inline( 0, -pcm_amp, this->pcm_buf ); if ( pcm_buf ) pcm_synth.offset_inline( 0, pcm_amp, pcm_buf ); }*/ this->pcm_buf = pcm_buf; } fm.write1( data, data2 ); } else if ( cmd == 3 ) { apu.write_data( 0, data ); } else { // to do: many GYM streams are full of errors, and error count should // reflect cases where music is really having problems //log_error(); --pos; // put data back } } if ( pos >= file_end() ) { // Reached end check( pos == file_end() ); if ( loop_begin ) pos = loop_begin; else set_track_ended(); } this->pos = pos; // PCM if ( pcm_buf && pcm_size ) run_pcm( pcm, pcm_size ); prev_pcm_count = pcm_size; } inline int Gym_Emu::play_frame( blip_time_t blip_time, int sample_count, sample_t buf [] ) { if ( !track_ended() ) parse_frame(); apu.end_frame( blip_time ); memset( buf, 0, sample_count * sizeof *buf ); fm.run( sample_count >> 1, buf ); return sample_count; } int Gym_Emu::play_frame_( void* p, blip_time_t a, int b, sample_t c [] ) { return STATIC_CAST(Gym_Emu*,p)->play_frame( a, b, c ); } blargg_err_t Gym_Emu::play_( int count, sample_t out [] ) { resampler.dual_play( count, out, stereo_buf ); return blargg_ok; } blargg_err_t Gym_Emu::hash_( Hash_Function& out ) const { hash_gym_file( header(), log_begin(), file_end() - log_begin(), out ); return blargg_ok; }