// Game_Music_Emu 0.6-pre. http://www.slack.net/~ant/

#include "ay_apu.h"

/* Copyright (C) 2006-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"

// Emulation inaccuracies:
// * Noise isn't run when not in use
// * Changes to envelope and noise periods are delayed until next reload
// * Super-sonic tone should attenuate output to about 60%, not 50%

// Tones above this frequency are treated as disabled tone at half volume.
// Power of two is more efficient (avoids division).
int const inaudible_freq = 16384;

int const period_factor = 16;

static byte const amp_table [16] =
{
#define ENTRY( n ) (byte) (n * ay_amp_range + 0.5)
	// With channels tied together and 1K resistor to ground (as datasheet recommends),
	// output nearly matches logarithmic curve as claimed. Approx. 1.5 dB per step.
	ENTRY(0.000000),ENTRY(0.007813),ENTRY(0.011049),ENTRY(0.015625),
	ENTRY(0.022097),ENTRY(0.031250),ENTRY(0.044194),ENTRY(0.062500),
	ENTRY(0.088388),ENTRY(0.125000),ENTRY(0.176777),ENTRY(0.250000),
	ENTRY(0.353553),ENTRY(0.500000),ENTRY(0.707107),ENTRY(1.000000),
	
	/*
	// Measured from an AY-3-8910A chip with date code 8611.
	
	// Direct voltages without any load (very linear)
	ENTRY(0.000000),ENTRY(0.046237),ENTRY(0.064516),ENTRY(0.089785),
	ENTRY(0.124731),ENTRY(0.173118),ENTRY(0.225806),ENTRY(0.329032),
	ENTRY(0.360215),ENTRY(0.494624),ENTRY(0.594624),ENTRY(0.672043),
	ENTRY(0.766129),ENTRY(0.841935),ENTRY(0.926882),ENTRY(1.000000),
	// With only some load
	ENTRY(0.000000),ENTRY(0.011940),ENTRY(0.017413),ENTRY(0.024876),
	ENTRY(0.036318),ENTRY(0.054229),ENTRY(0.072637),ENTRY(0.122388),
	ENTRY(0.174129),ENTRY(0.239303),ENTRY(0.323881),ENTRY(0.410945),
	ENTRY(0.527363),ENTRY(0.651741),ENTRY(0.832338),ENTRY(1.000000),
	*/
#undef ENTRY
};

static byte const modes [8] =
{
#define MODE( a0,a1, b0,b1, c0,c1 ) \
		(a0 | a1<<1 | b0<<2 | b1<<3 | c0<<4 | c1<<5)
	MODE( 1,0, 1,0, 1,0 ),
	MODE( 1,0, 0,0, 0,0 ),
	MODE( 1,0, 0,1, 1,0 ),
	MODE( 1,0, 1,1, 1,1 ),
	MODE( 0,1, 0,1, 0,1 ),
	MODE( 0,1, 1,1, 1,1 ),
	MODE( 0,1, 1,0, 0,1 ),
	MODE( 0,1, 0,0, 0,0 ),
};

static void set_output( struct Ay_Apu* this, struct Blip_Buffer* b )
{
	int i;
	for ( i = 0; i < ay_osc_count; ++i )
		Ay_apu_set_output( this, i, b );
}

void Ay_apu_init( struct Ay_Apu* this )
{
	Synth_init( &this->synth_ );
	
	// build full table of the upper 8 envelope waveforms
	int m;
	for ( m = 8; m--; )
	{
		byte* out = this->env_modes [m];
		int x, y, flags = modes [m];
		for ( x = 3; --x >= 0; )
		{
			int amp = flags & 1;
			int end = flags >> 1 & 1;
			int step = end - amp;
			amp *= 15;
			for ( y = 16; --y >= 0; )
			{
				*out++ = amp_table [amp];
				amp += step;
			}
			flags >>= 2;
		}
	}
	
	set_output( this, NULL );
	Ay_apu_volume( this, (int)FP_ONE_VOLUME );
	Ay_apu_reset( this );
}

void Ay_apu_reset( struct Ay_Apu* this )
{
	this->addr_       = 0;
	this->last_time   = 0;
	this->noise_delay = 0;
	this->noise_lfsr  = 1;
	
	struct osc_t* osc;
	for ( osc = &this->oscs [ay_osc_count]; osc != this->oscs; )
	{
		osc--;
		osc->period   = period_factor;
		osc->delay    = 0;
		osc->last_amp = 0;
		osc->phase    = 0;
	}
	
	int i;
	for ( i = sizeof this->regs; --i >= 0; )
		this->regs [i] = 0;
	this->regs [7] = 0xFF;
	write_data_( this, 13, 0 );
}

int Ay_apu_read( struct Ay_Apu* this )
{
	static byte const masks [ay_reg_count] = { 
		0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0x1F, 0x3F,
		0x1F, 0x1F, 0x1F, 0xFF, 0xFF, 0x0F, 0x00, 0x00
	};
	return this->regs [this->addr_] & masks [this->addr_];
}

void write_data_( struct Ay_Apu* this, int addr, int data )
{
	assert( (unsigned) addr < ay_reg_count );
	
	/* if ( (unsigned) addr >= 14 )
		dprintf( "Wrote to I/O port %02X\n", (int) addr ); */
	
	// envelope mode
	if ( addr == 13 )
	{
		if ( !(data & 8) ) // convert modes 0-7 to proper equivalents
			data = (data & 4) ? 15 : 9;
		this->env_wave = this->env_modes [data - 7];
		this->env_pos = -48;
		this->env_delay = 0; // will get set to envelope period in run_until()
	}
	this->regs [addr] = data;
	
	// handle period changes accurately
	int i = addr >> 1;
	if ( i < ay_osc_count )
	{
		blip_time_t period = (this->regs [i * 2 + 1] & 0x0F) * (0x100 * period_factor) +
				this->regs [i * 2] * period_factor;
		if ( !period )
			period = period_factor;
		
		// adjust time of next timer expiration based on change in period
		struct osc_t* osc = &this->oscs [i];
		if ( (osc->delay += period - osc->period) < 0 )
			osc->delay = 0;
		osc->period = period;
	}
	
	// TODO: same as above for envelope timer, and it also has a divide by two after it
}

int const noise_off = 0x08;
int const tone_off  = 0x01;

void run_until( struct Ay_Apu* this, blip_time_t final_end_time )
{
	require( final_end_time >= this->last_time );
	
	// noise period and initial values
	blip_time_t const noise_period_factor = period_factor * 2; // verified
	blip_time_t noise_period = (this->regs [6] & 0x1F) * noise_period_factor;
	if ( !noise_period )
		noise_period = noise_period_factor;
	blip_time_t const old_noise_delay = this->noise_delay;
	unsigned const old_noise_lfsr = this->noise_lfsr;
	
	// envelope period
	blip_time_t const env_period_factor = period_factor * 2; // verified
	blip_time_t env_period = (this->regs [12] * 0x100 + this->regs [11]) * env_period_factor;
	if ( !env_period )
		env_period = env_period_factor; // same as period 1 on my AY chip
	if ( !this->env_delay )
		this->env_delay = env_period;
	
	// run each osc separately
	int index;
	for ( index = 0; index < ay_osc_count; index++ )
	{
		struct osc_t* const osc = &this->oscs [index];
		int osc_mode = this->regs [7] >> index;
		
		// output
		struct Blip_Buffer* const osc_output = osc->output;
		if ( !osc_output )
			continue;
		Blip_set_modified( osc_output );
		
		// period
		int half_vol = 0;
		blip_time_t inaudible_period = (unsigned) (Blip_clock_rate( osc_output ) +
				inaudible_freq) / (unsigned) (inaudible_freq * 2);
		if ( osc->period <= inaudible_period && !(osc_mode & tone_off) )
		{
			half_vol = 1; // Actually around 60%, but 50% is close enough
			osc_mode |= tone_off;
		}
		
		// envelope
		blip_time_t start_time = this->last_time;
		blip_time_t end_time   = final_end_time;
		int const vol_mode = this->regs [0x08 + index];
		int volume = amp_table [vol_mode & 0x0F] >> half_vol;
		int osc_env_pos = this->env_pos;
		if ( vol_mode & 0x10 )
		{
			volume = this->env_wave [osc_env_pos] >> half_vol;
			// use envelope only if it's a repeating wave or a ramp that hasn't finished
			if ( !(this->regs [13] & 1) || osc_env_pos < -32 )
			{
				end_time = start_time + this->env_delay;
				if ( end_time >= final_end_time )
					end_time = final_end_time;
				
				//if ( !(regs [12] | regs [11]) )
				//  dprintf( "Used envelope period 0\n" );
			}
			else if ( !volume )
			{
				osc_mode = noise_off | tone_off;
			}
		}
		else if ( !volume )
		{
			osc_mode = noise_off | tone_off;
		}
		
		// tone time
		blip_time_t const period = osc->period;
		blip_time_t time = start_time + osc->delay;
		if ( osc_mode & tone_off ) // maintain tone's phase when off
		{
			int count = (final_end_time - time + period - 1) / period;
			time += count * period;
			osc->phase ^= count & 1;
		}
		
		// noise time
		blip_time_t ntime = final_end_time;
		unsigned noise_lfsr = 1;
		if ( !(osc_mode & noise_off) )
		{
			ntime = start_time + old_noise_delay;
			noise_lfsr = old_noise_lfsr;
			//if ( (regs [6] & 0x1F) == 0 )
			//  dprintf( "Used noise period 0\n" );
		}
		
		// The following efficiently handles several cases (least demanding first):
		// * Tone, noise, and envelope disabled, where channel acts as 4-bit DAC
		// * Just tone or just noise, envelope disabled
		// * Envelope controlling tone and/or noise
		// * Tone and noise disabled, envelope enabled with high frequency
		// * Tone and noise together
		// * Tone and noise together with envelope
		
		// this loop only runs one iteration if envelope is disabled. If envelope
		// is being used as a waveform (tone and noise disabled), this loop will
		// still be reasonably efficient since the bulk of it will be skipped.
		while ( 1 )
		{
			// current amplitude
			int amp = 0;
			if ( (osc_mode | osc->phase) & 1 & (osc_mode >> 3 | noise_lfsr) )
				amp = volume;
			{
				int delta = amp - osc->last_amp;
				if ( delta )
				{
					osc->last_amp = amp;
					Synth_offset( &this->synth_, start_time, delta, osc_output );
				}
			}
			
			// Run wave and noise interleved with each catching up to the other.
			// If one or both are disabled, their "current time" will be past end time,
			// so there will be no significant performance hit.
			if ( ntime < end_time || time < end_time )
			{
				// Since amplitude was updated above, delta will always be +/- volume,
				// so we can avoid using last_amp every time to calculate the delta.
				int delta = amp * 2 - volume;
				int delta_non_zero = delta != 0;
				int phase = osc->phase | (osc_mode & tone_off); assert( tone_off == 0x01 );
				do
				{
					// run noise
					blip_time_t end = end_time;
					if ( end_time > time ) end = time;
					if ( phase & delta_non_zero )
					{
						while ( ntime <= end ) // must advance *past* time to avoid hang
						{
							int changed = noise_lfsr + 1;
							noise_lfsr = (-(noise_lfsr & 1) & 0x12000) ^ (noise_lfsr >> 1);
							if ( changed & 2 )
							{
								delta = -delta;
								Synth_offset( &this->synth_, ntime, delta, osc_output );
							}
							ntime += noise_period;
						}
					}
					else
					{
						// 20 or more noise periods on average for some music
						int remain = end - ntime;
						int count = remain / noise_period;
						if ( remain >= 0 )
							ntime += noise_period + count * noise_period;
					}
					
					// run tone
					end = end_time;
					if ( end_time > ntime ) end = ntime;
					if ( noise_lfsr & delta_non_zero )
					{
						while ( time < end )
						{
							delta = -delta;
							Synth_offset( &this->synth_, time, delta, osc_output );
							time += period;
							
							// alternate (less-efficient) implementation
							//phase ^= 1;
						}
						phase = (unsigned) (-delta) >> (CHAR_BIT * sizeof (unsigned) - 1);
						check( phase == (delta > 0) );
					}
					else
					{
						// loop usually runs less than once
						//SUB_CASE_COUNTER( (time < end) * (end - time + period - 1) / period );
						
						while ( time < end )
						{
							time += period;
							phase ^= 1;
						}
					}
				}
				while ( time < end_time || ntime < end_time );
				
				osc->last_amp = (delta + volume) >> 1;
				if ( !(osc_mode & tone_off) )
					osc->phase = phase;
			}
			
			if ( end_time >= final_end_time )
				break; // breaks first time when envelope is disabled
			
			// next envelope step
			if ( ++osc_env_pos >= 0 )
				osc_env_pos -= 32;
			volume = this->env_wave [osc_env_pos] >> half_vol;
			
			start_time = end_time;
			end_time += env_period;
			if ( end_time > final_end_time )
				end_time = final_end_time;
		}
		osc->delay = time - final_end_time;
		
		if ( !(osc_mode & noise_off) )
		{
			this->noise_delay = ntime - final_end_time;
			this->noise_lfsr = noise_lfsr;
		}
	}
	
	// TODO: optimized saw wave envelope?
	
	// maintain envelope phase
	blip_time_t remain = final_end_time - this->last_time - this->env_delay;
	if ( remain >= 0 )
	{
		int count = (remain + env_period) / env_period;
		this->env_pos += count;
		if ( this->env_pos >= 0 )
			this->env_pos = (this->env_pos & 31) - 32;
		remain -= count * env_period;
		assert( -remain <= env_period );
	}
	this->env_delay = -remain;
	assert( this->env_delay > 0 );
	assert( this->env_pos < 0 );
	
	this->last_time = final_end_time;
}
