
// Gb_Snd_Emu 0.1.3. http://www.slack.net/~ant/libs/

#include "Gb_Apu.h"

#include <string.h>

/* Copyright (C) 2003-2005 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */

#include BLARGG_SOURCE_BEGIN

const int trigger = 0x80;

// Gb_Osc

Gb_Osc::Gb_Osc()
{
	output = NULL;
	outputs [0] = NULL;
	outputs [1] = NULL;
	outputs [2] = NULL;
	outputs [3] = NULL;
}

void Gb_Osc::reset()
{
	delay = 0;
	last_amp = 0;
	period = 2048;
	volume = 0;
	frequency = 0;
	length = 0;
	enabled = false;
	length_enabled = false;
	output_select = 3;
	output = outputs [output_select];
}

void Gb_Osc::clock_length()
{
	if ( length_enabled && length )
		--length;
}

void Gb_Osc::write_register( int reg, int value )
{
	if ( reg == 4 )
		length_enabled = value & 0x40;
}

// Gb_Env

void Gb_Env::reset()
{
	env_period = 0;
	env_dir = 0;
	env_delay = 0;
	new_env_period = 0;
	new_env_dir = 0;
	new_volume = 0;
	Gb_Osc::reset();
}

Gb_Env::Gb_Env() {
}

void Gb_Env::clock_envelope()
{
	if ( env_delay && !--env_delay ) {
		env_delay = env_period;
		if ( env_dir ) {
			if ( volume < 15 )
				++volume;
		}
		else if ( volume > 0 ) {
			--volume;
		}
	}
}

void Gb_Env::write_register( int reg, int value )
{
	if ( reg == 2 ) {
		new_env_period = value & 7;
		new_env_dir = value & 8;
		new_volume = value >> 4;
		if ( value == 0 && volume )
			// to do: find correct behavior
			volume = 0;
		//enabled = new_volume != 0;
		enabled = true;
	}
	else if ( reg == 4 && (value & trigger) ) {
		env_period = new_env_period;
		env_delay = new_env_period;
		env_dir = new_env_dir;
		volume = new_volume;
	}
	Gb_Osc::write_register( reg, value );
}

// Gb_Square

void Gb_Square::reset()
{
	phase = 1;
	duty = 1;
	
	sweep_period = 0;
	sweep_delay = 0;
	sweep_shift = 0;
	sweep_dir = 0;
	sweep_freq = 0;
	
	Gb_Env::reset();
}

Gb_Square::Gb_Square()
{
	has_sweep = false;
}

void Gb_Square::clock_sweep()
{
	if ( sweep_period && sweep_delay && !--sweep_delay ) {
		sweep_delay = sweep_period;
		frequency = sweep_freq;
		period = (2048 - frequency) * 4;
		
		int offset = sweep_freq >> sweep_shift;
		if ( sweep_dir )
			offset = -offset;
		sweep_freq += offset;
		if ( sweep_freq < 0 || sweep_freq >= 2048 ) {
			sweep_delay = 0;
			sweep_freq = 2048; // stop sound output
		}
	}
}

void Gb_Square::write_register( int reg, int value )
{
	switch ( reg ) {
		case 0:
			sweep_period = (value >> 4) & 3;
			sweep_shift = value & 7;
			sweep_dir = value & 0x08;
			break;
		
		case 1:
			length = 64 - (value & 0x3f);
			duty = (value >> 5) & 6; // duty = { 1, 2, 4, 6 }
			if ( !duty )
				duty = 1;
			break;
		
		case 3:
			frequency = (frequency & ~0xFF) + value;
			break;
		
		case 4:
			frequency = (value & 7) * 0x100 + (frequency & 0xFF);
			if ( value & trigger ) {
				sweep_freq = frequency;
				if ( has_sweep && sweep_period && sweep_shift ) {
					sweep_delay = 1;
					clock_sweep();
					sweep_delay = sweep_period;
				}
				enabled = true;
			}
			break;
	}
	
	period = (2048 - frequency) * 4;
	
	Gb_Env::write_register( reg, value );
}

void Gb_Square::run( gb_time_t time, gb_time_t end_time )
{
	if ( !enabled || (!length && length_enabled) || !volume || sweep_freq == 2048 ) {
		if ( last_amp ) {
			synth->offset( time, -last_amp, output );
			last_amp = 0;
		}
		delay = 0;
	}
	else
	{
		int amp = (phase < duty) ? volume : -volume;
		if ( amp != last_amp ) {
			synth->offset( time, amp - last_amp, output );
			last_amp = amp;
		}
		
		time += delay;
		if ( time < end_time )
		{
			Blip_Buffer* const output = this->output;
			const int duty = this->duty;
			int phase = this->phase;
			amp *= 2;
			do {
				phase = (phase + 1) & 7;
				if ( phase == 0 || phase == duty ) {
					amp = -amp;
					synth->offset_inline( time, amp, output );
				}
				time += period;
			}
			while ( time < end_time );
			
			this->phase = phase;
			last_amp = amp >> 1;
		}
		delay = time - end_time;
	}
}


// Gb_Wave

void Gb_Wave::reset()
{
	volume_shift = 0;
	wave_pos = 0;
	memset( wave, 0, sizeof wave );
	Gb_Osc::reset();
}

Gb_Wave::Gb_Wave() {
}

void Gb_Wave::write_register( int reg, int value )
{
	switch ( reg ) {
		case 0:
			enabled = value & 0x80;
			break;
		
		case 1:
			length = 256 - value;
			break;
		
		case 2:
			volume = ((value >> 5) & 3);
			volume_shift = (volume - 1) & 7; // silence = 7
			break;
		
		case 3:
			frequency = (frequency & ~0xFF) + value;
			break;
		
		case 4:
			frequency = (value & 7) * 0x100 + (frequency & 0xFF);
			//if ( value & trigger )
			//  wave_pos = 0;
			break;
		
	}
	
	period = (2048 - frequency) * 2;
	
	Gb_Osc::write_register( reg, value );
}

void Gb_Wave::run( gb_time_t time, gb_time_t end_time )
{
	if ( !enabled || (!length && length_enabled) || !volume ) {
		if ( last_amp ) {
			synth.offset( time, -last_amp, output );
			last_amp = 0;
		}
		delay = 0;
	}
	else
	{
		// wave data or shift may have changed
		int diff = (wave [wave_pos] >> volume_shift) * 2 - last_amp;
		if ( diff ) {
			last_amp += diff;
			synth.offset( time, diff, output );
		}
		
		time += delay;
		if ( time < end_time )
		{
		 	unsigned wave_pos = this->wave_pos;
		 	
			do {
				wave_pos = (wave_pos + 1) % wave_size;
				int amp = (wave [wave_pos] >> volume_shift) * 2;
				int diff = amp - last_amp;
				if ( diff ) {
					last_amp = amp;
					synth.offset_inline( time, diff, output );
				}
				time += period;
			}
			while ( time < end_time );
			
			this->wave_pos = wave_pos;
		}
		delay = time - end_time;
	}
}


// Gb_Noise

void Gb_Noise::reset()
{
	bits = 1;
	tap = 14;
	Gb_Env::reset();
}

Gb_Noise::Gb_Noise() {
}

void Gb_Noise::write_register( int reg, int value )
{
	if ( reg == 1 ) {
		length = 64 - (value & 0x3f);
	}
	else if ( reg == 3 ) {
		tap = 14 - (value & 8);
		// noise formula and frequency tested against Metroid 2 and Zelda LA
		int divisor = (value & 7) * 16;
		if ( !divisor )
			divisor = 8;
		period = divisor << (value >> 4);
	}
	else if ( reg == 4 && value & trigger ) {
		bits = ~0u;
	}
	
	Gb_Env::write_register( reg, value );
}

#include BLARGG_ENABLE_OPTIMIZER

void Gb_Noise::run( gb_time_t time, gb_time_t end_time )
{
	if ( !enabled || (!length && length_enabled) || !volume ) {
		if ( last_amp ) {
			synth.offset( time, -last_amp, output );
			last_amp = 0;
		}
		delay = 0;
	}
	else
	{
		int amp = bits & 1 ? -volume : volume;
		if ( amp != last_amp ) {
			synth.offset( time, amp - last_amp, output );
			last_amp = amp;
		}
		
		time += delay;
		if ( time < end_time )
		{
			Blip_Buffer* const output = this->output;
			// keep parallel resampled time to eliminate multiplication in the loop
			const Blip_Buffer::resampled_time_t resampled_period =
					output->resampled_duration( period );
			Blip_Buffer::resampled_time_t resampled_time = output->resampled_time( time );
			const unsigned mask = ~(1u << tap);
			unsigned bits = this->bits;
			amp *= 2;
			
			do {
				unsigned feedback = bits;
				bits >>= 1;
				feedback = 1 & (feedback ^ bits);
				time += period;
				bits = (feedback << tap) | (bits & mask);
				// feedback just happens to be true only when the level needs to change
				// (the previous and current bits are different)
				if ( feedback ) {
					amp = -amp;
					synth.offset_resampled( resampled_time, amp, output );
				}
				resampled_time += resampled_period;
			}
			while ( time < end_time );
			
			this->bits = bits;
			last_amp = amp >> 1;
		}
		delay = time - end_time;
	}
}

