package audio.gme; // Sega Master System, BBC Micro VGM 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 VgmEmu extends ClassicEmu { protected int loadFile_( byte [] data ) { if ( !isHeader( data, "Vgm " ) ) error( "Not a VGM file" ); // TODO: use custom noise taps if present // Data and loop this.data = data; loopBegin = getLE32( data, 28 ) + 28; if ( loopBegin <= 28 ) { loopBegin = data.length; } else if ( data [data.length - 1] != cmd_end ) { data = DataReader.resize( data, data.length + 1 ); data [data.length - 1] = cmd_end; } // PSG clock rate int clockRate = getLE32( data, 0x0C ); if ( clockRate == 0 ) clockRate = 3579545; psgFactor = (int) ((float) psgTimeUnit / vgmRate * clockRate + 0.5); // FM clock rate fm_clock_rate = getLE32( data, 0x2C ); fm = null; if ( fm_clock_rate != 0 ) { fm = new YM2612(); buf.setVolume( 0.7 ); fm.init( fm_clock_rate, sampleRate() ); } else { buf.setVolume( 1.0 ); } setClockRate( clockRate ); apu.setOutput( buf.center(), buf.left(), buf.right() ); return 1; } // private static final int vgmRate = 44100; static final int psgTimeBits = 12; static final int psgTimeUnit = 1 << psgTimeBits; final SmsApu apu = new SmsApu(); YM2612 fm; int fm_clock_rate; int pos; byte [] data; int delay; int psgFactor; int loopBegin; final int [] fm_buf_lr = new int [48000 / 10 * 2]; int fm_pos; int dac_disabled; // -1 if disabled int pcm_data; int pcm_pos; int dac_amp; static final int cmd_gg_stereo = 0x4F; static final int cmd_psg = 0x50; static final int cmd_ym2612_port0 = 0x52; static final int cmd_ym2612_port1 = 0x53; static final int cmd_delay = 0x61; static final int cmd_delay_735 = 0x62; static final int cmd_delay_882 = 0x63; static final int cmd_end = 0x66; static final int cmd_data_block = 0x67; static final int cmd_short_delay = 0x70; static final int cmd_pcm_delay = 0x80; static final int cmd_pcm_seek = 0xE0; static final int ym2612_dac_port = 0x2A; static final int pcm_block_type = 0x00; public void startTrack( int track ) { super.startTrack( track ); pos = 0x40; delay = 0; pcm_data = pos; pcm_pos = pos; dac_amp = -1; apu.reset(); if ( fm != null ) fm.reset(); } private int toPSGTime( int vgmTime ) { return (vgmTime * psgFactor + psgTimeUnit / 2) >> psgTimeBits; } private int toFMTime( int vgmTime ) { return countSamples( toPSGTime( vgmTime ) ); } private void runFM( int vgmTime ) { int count = toFMTime( vgmTime ) - fm_pos; if ( count > 0 ) { fm.update( fm_buf_lr, fm_pos, count ); fm_pos += count; } } private void write_pcm( int vgmTime, int amp ) { int blip_time = toPSGTime( vgmTime ); int old = dac_amp; int delta = amp - old; dac_amp = amp; if ( old >= 0 ) // first write is ignored, to avoid click buf.center().addDelta( blip_time, delta * 300 ); else dac_amp |= dac_disabled; } protected int runMsec( int msec ) { final int duration = vgmRate / 100 * msec / 10; { int sampleCount = toFMTime( duration ); java.util.Arrays.fill( fm_buf_lr, 0, sampleCount*2, 0 ); } fm_pos = 0; int time = delay; while ( time < duration ) { int cmd = cmd_end; if ( pos < data.length ) cmd = data [pos++] & 0xFF; switch ( cmd ) { case cmd_end: pos = loopBegin; break; case cmd_delay_735: time += 735; break; case cmd_delay_882: time += 882; break; case cmd_gg_stereo: apu.writeGG( toPSGTime( time ), data [pos++] & 0xFF ); break; case cmd_psg: apu.writeData( toPSGTime( time ), data [pos++] & 0xFF ); break; case cmd_ym2612_port0: if ( fm != null ) { int port = data [pos++] & 0xFF; int val = data [pos++] & 0xFF; if ( port == ym2612_dac_port ) { write_pcm( time, val ); } else { if ( port == 0x2B ) { dac_disabled = (val >> 7 & 1) - 1; dac_amp |= dac_disabled; } runFM( time ); fm.write0( port, val ); } } break; case cmd_ym2612_port1: if ( fm != null ) { runFM( time ); int port = data [pos++] & 0xFF; fm.write1( port, data [pos++] & 0xFF ); } break; case cmd_delay: time += (data [pos + 1] & 0xFF) * 0x100 + (data [pos] & 0xFF); pos += 2; break; case cmd_data_block: if ( data [pos++] != cmd_end ) logError(); int type = data [pos++]; long size = getLE32( data, pos ); pos += 4; if ( type == pcm_block_type ) pcm_data = pos; pos += size; break; case cmd_pcm_seek: pcm_pos = pcm_data + getLE32( data, pos ); pos += 4; break; default: switch ( cmd & 0xF0 ) { case cmd_pcm_delay: write_pcm( time, data [pcm_pos++] & 0xFF ); time += cmd & 0x0F; break; case cmd_short_delay: time += (cmd & 0x0F) + 1; break; case 0x50: pos += 2; break; default: logError(); break; } } } if ( fm != null ) runFM( duration ); int endTime = toPSGTime( duration ); delay = time - duration; apu.endFrame( endTime ); if ( pos >= data.length ) { setTrackEnded(); if ( pos > data.length ) { pos = data.length; logError(); // went past end } } fm_pos = 0; return endTime; } protected void mixSamples( byte [] out, int out_off, int count ) { if ( fm == null ) return; out_off *= 2; int in_off = fm_pos; while ( --count >= 0 ) { int s = (out [out_off] << 8) + (out [out_off + 1] & 0xFF); s = (s >> 2) + fm_buf_lr [in_off]; in_off++; if ( (short) s != s ) s = (s >> 31) ^ 0x7FFF; out [out_off] = (byte) (s >> 8); out_off++; out [out_off] = (byte) s ; out_off++; } fm_pos = in_off; } }