Emulator/GbaAudio.Registers.cs
namespace sGBA;

public partial class GbaAudio
{
	public void WriteRegister( uint offset, ushort value )
	{
		if ( offset >= 0x60 && offset < 0x80 && !Enable )
			return;

		if ( offset != 0x82 )
			FlushSamples();

		switch ( offset )
		{
			case 0x60:
				Sound1CntL = value;
				WriteNR10( (byte)(value & 0xFF) );
				break;
			case 0x62:
				Sound1CntH = value;
				WriteNR11( (byte)(value & 0xFF) );
				WriteNR12( (byte)(value >> 8) );
				break;
			case 0x64:
				Sound1CntX = value;
				WriteNR13( (byte)(value & 0xFF) );
				WriteNR14( (byte)(value >> 8) );
				break;

			case 0x68:
				Sound2CntL = value;
				WriteNR21( (byte)(value & 0xFF) );
				WriteNR22( (byte)(value >> 8) );
				break;
			case 0x6C:
				Sound2CntH = value;
				WriteNR23( (byte)(value & 0xFF) );
				WriteNR24( (byte)(value >> 8) );
				break;

			case 0x70:
				Sound3CntL = value;
				WriteNR30( (byte)(value & 0xFF) );
				break;
			case 0x72:
				Sound3CntH = value;
				WriteNR31( (byte)(value & 0xFF) );
				_ch3Volume = (value >> 13) & 7;
				break;
			case 0x74:
				Sound3CntX = value;
				WriteNR33( (byte)(value & 0xFF) );
				WriteNR34( (byte)(value >> 8) );
				break;

			case 0x78:
				Sound4CntL = value;
				WriteNR41( (byte)(value & 0xFF) );
				WriteNR42( (byte)(value >> 8) );
				break;
			case 0x7C:
				Sound4CntH = value;
				WriteNR43( (byte)(value & 0xFF) );
				WriteNR44( (byte)(value >> 8) );
				break;

			case 0x80:
				SoundCntL = value;
				WriteNR50( (byte)(value & 0xFF) );
				WriteNR51( (byte)(value >> 8) );
				break;
			case 0x82:
				WriteSoundCntH( value );
				break;
			case 0x84:
				WriteSoundCntX( value );
				break;
			case 0x88:
				SoundBias = value;
				break;
		}
	}

	public void WriteRegisterByte( uint regOffset, bool highByte, byte value )
	{
		if ( regOffset != 0x82 )
			FlushSamples();

		switch ( regOffset )
		{
			case 0x60:
				if ( !highByte )
				{
					Sound1CntL = (ushort)((Sound1CntL & 0xFF00) | value);
					WriteNR10( value );
				}
				break;
			case 0x62:
				if ( !highByte )
				{
					Sound1CntH = (ushort)((Sound1CntH & 0xFF00) | value);
					WriteNR11( value );
				}
				else
				{
					Sound1CntH = (ushort)((Sound1CntH & 0x00FF) | (value << 8));
					WriteNR12( value );
				}
				break;
			case 0x64:
				if ( !highByte )
				{
					Sound1CntX = (ushort)((Sound1CntX & 0xFF00) | value);
					WriteNR13( value );
				}
				else
				{
					Sound1CntX = (ushort)((Sound1CntX & 0x00FF) | (value << 8));
					WriteNR14( value );
				}
				break;
			case 0x68:
				if ( !highByte )
				{
					Sound2CntL = (ushort)((Sound2CntL & 0xFF00) | value);
					WriteNR21( value );
				}
				else
				{
					Sound2CntL = (ushort)((Sound2CntL & 0x00FF) | (value << 8));
					WriteNR22( value );
				}
				break;
			case 0x6C:
				if ( !highByte )
				{
					Sound2CntH = (ushort)((Sound2CntH & 0xFF00) | value);
					WriteNR23( value );
				}
				else
				{
					Sound2CntH = (ushort)((Sound2CntH & 0x00FF) | (value << 8));
					WriteNR24( value );
				}
				break;
			case 0x70:
				if ( !highByte )
				{
					Sound3CntL = (ushort)((Sound3CntL & 0xFF00) | value);
					WriteNR30( value );
				}
				break;
			case 0x72:
				if ( !highByte )
				{
					Sound3CntH = (ushort)((Sound3CntH & 0xFF00) | value);
					WriteNR31( value );
				}
				else
				{
					Sound3CntH = (ushort)((Sound3CntH & 0x00FF) | (value << 8));
					_ch3Volume = (value >> 5) & 7;
				}
				break;
			case 0x74:
				if ( !highByte )
				{
					Sound3CntX = (ushort)((Sound3CntX & 0xFF00) | value);
					WriteNR33( value );
				}
				else
				{
					Sound3CntX = (ushort)((Sound3CntX & 0x00FF) | (value << 8));
					WriteNR34( value );
				}
				break;
			case 0x78:
				if ( !highByte )
				{
					Sound4CntL = (ushort)((Sound4CntL & 0xFF00) | value);
					WriteNR41( value );
				}
				else
				{
					Sound4CntL = (ushort)((Sound4CntL & 0x00FF) | (value << 8));
					WriteNR42( value );
				}
				break;
			case 0x7C:
				if ( !highByte )
				{
					Sound4CntH = (ushort)((Sound4CntH & 0xFF00) | value);
					WriteNR43( value );
				}
				else
				{
					Sound4CntH = (ushort)((Sound4CntH & 0x00FF) | (value << 8));
					WriteNR44( value );
				}
				break;
			case 0x80:
				if ( !highByte )
				{
					SoundCntL = (ushort)((SoundCntL & 0xFF00) | value);
					WriteNR50( value );
				}
				else
				{
					SoundCntL = (ushort)((SoundCntL & 0x00FF) | (value << 8));
					WriteNR51( value );
				}
				break;
			case 0x82:
				if ( !highByte )
				{
					SoundCntH = (ushort)((SoundCntH & 0xFF00) | value);
					_psgVolume = value & 3;
					_volumeChA = (value & 4) != 0;
					_volumeChB = (value & 8) != 0;
				}
				else
				{
					SoundCntH = (ushort)((SoundCntH & 0x00FF) | (value << 8));
					_chARight = (value & 1) != 0;
					_chALeft = (value & 2) != 0;
					_chATimer = (value & 4) != 0;
					if ( (value & 8) != 0 )
					{
						_fifoA.Write = _fifoA.Read = 0;
					}
					_chBRight = (value & 0x10) != 0;
					_chBLeft = (value & 0x20) != 0;
					_chBTimer = (value & 0x40) != 0;
					if ( (value & 0x80) != 0 )
					{
						_fifoB.Write = _fifoB.Read = 0;
					}
				}
				break;
			case 0x84:
				if ( !highByte )
				{
					WriteSoundCntX( value );
				}
				break;
			case 0x88:
				if ( !highByte )
					SoundBias = (ushort)((SoundBias & 0xFF00) | value);
				else
					SoundBias = (ushort)((SoundBias & 0x00FF) | (value << 8));
				break;
		}
	}

	public ushort ReadRegister( uint offset )
	{
		switch ( offset )
		{
			case 0x60: return (ushort)(Sound1CntL & 0x007F);
			case 0x62: return (ushort)(Sound1CntH & 0xFFC0);
			case 0x64: return (ushort)(Sound1CntX & 0x4000);
			case 0x68: return (ushort)(Sound2CntL & 0xFFC0);
			case 0x6C: return (ushort)(Sound2CntH & 0x4000);
			case 0x70: return (ushort)(Sound3CntL & 0x00E0);
			case 0x72: return (ushort)(Sound3CntH & 0xE000);
			case 0x74: return (ushort)(Sound3CntX & 0x4000);
			case 0x78: return (ushort)(Sound4CntL & 0xFF00);
			case 0x7C: return (ushort)(Sound4CntH & 0x40FF);
			case 0x80: return (ushort)(SoundCntL & 0xFF77);
			case 0x82: return (ushort)(SoundCntH & 0x770F);
			case 0x84:
				{
					ushort status = (ushort)(Enable ? 0x80 : 0);
					if ( _ch1Playing ) status |= 1;
					if ( _ch2Playing ) status |= 2;
					if ( _ch3Playing ) status |= 4;
					if ( _ch4Playing ) status |= 8;
					return status;
				}
			case 0x88: return SoundBias;
			default: return 0;
		}
	}

	private void WriteNR10( byte value )
	{
		_ch1SweepShift = value & 7;
		bool oldDir = _ch1SweepDirection;
		_ch1SweepDirection = (value & 8) != 0;
		if ( _ch1SweepOccurred && oldDir && !_ch1SweepDirection )
		{
			_ch1Playing = false;
		}
		_ch1SweepOccurred = false;
		_ch1SweepTime = (value >> 4) & 7;
		if ( _ch1SweepTime == 0 ) _ch1SweepTime = 8;
	}

	private void WriteNR11( byte value )
	{
		_ch1Duty = (value >> 6) & 3;
		_ch1Length = 64 - (value & 0x3F);
	}

	private void WriteNR12( byte value )
	{
		_ch1EnvStepTime = value & 7;
		_ch1EnvDirection = (value & 8) != 0;
		_ch1EnvInitVolume = (value >> 4) & 0xF;

		if ( _ch1EnvStepTime == 0 )
		{
			_ch1EnvVolume &= 0xF;
		}

		UpdateEnvelopeDead( ref _ch1EnvVolume, ref _ch1EnvStepTime, ref _ch1EnvDirection,
		ref _ch1EnvInitVolume, ref _ch1EnvDead, ref _ch1EnvNextStep );

		if ( _ch1EnvInitVolume == 0 && !_ch1EnvDirection )
		{
			_ch1Playing = false;
		}
	}

	private void WriteNR13( byte value )
	{
		_ch1Frequency = (_ch1Frequency & 0x700) | value;
	}

	private void WriteNR14( byte value )
	{
		_ch1Frequency = (_ch1Frequency & 0xFF) | ((value & 7) << 8);
		bool wasStop = _ch1Stop;
		_ch1Stop = (value & 0x40) != 0;

		if ( !wasStop && _ch1Stop && _ch1Length > 0 && (_frameSeqStep & 1) == 0 )
		{
			_ch1Length--;
			if ( _ch1Length == 0 )
				_ch1Playing = false;
		}

		if ( (value & 0x80) != 0 )
		{
			_ch1Playing = ResetEnvelope( ref _ch1EnvVolume, ref _ch1EnvStepTime,
			ref _ch1EnvDirection, ref _ch1EnvInitVolume, ref _ch1EnvDead, ref _ch1EnvNextStep );

			_ch1SweepRealFreq = _ch1Frequency;
			_ch1SweepStep = _ch1SweepTime;
			_ch1SweepEnable = (_ch1SweepTime != 8) || _ch1SweepShift != 0;
			_ch1SweepOccurred = false;

			if ( _ch1Playing && _ch1SweepShift > 0 )
			{
				_ch1Playing = UpdateSweep( true );
			}

			if ( _ch1Length == 0 )
			{
				_ch1Length = 64;
				if ( _ch1Stop && (_frameSeqStep & 1) == 0 )
					_ch1Length--;
			}

			_ch1Sample = DutyTable[_ch1Duty * 8 + _ch1DutyIndex] * _ch1EnvVolume;
			_ch1LastUpdate = _totalCycles;
		}
	}

	private void WriteNR21( byte value )
	{
		_ch2Duty = (value >> 6) & 3;
		_ch2Length = 64 - (value & 0x3F);
	}

	private void WriteNR22( byte value )
	{
		_ch2EnvStepTime = value & 7;
		_ch2EnvDirection = (value & 8) != 0;
		_ch2EnvInitVolume = (value >> 4) & 0xF;

		if ( _ch2EnvStepTime == 0 )
		{
			_ch2EnvVolume &= 0xF;
		}

		UpdateEnvelopeDead( ref _ch2EnvVolume, ref _ch2EnvStepTime, ref _ch2EnvDirection,
		ref _ch2EnvInitVolume, ref _ch2EnvDead, ref _ch2EnvNextStep );

		if ( _ch2EnvInitVolume == 0 && !_ch2EnvDirection )
		{
			_ch2Playing = false;
		}
	}

	private void WriteNR23( byte value )
	{
		_ch2Frequency = (_ch2Frequency & 0x700) | value;
	}

	private void WriteNR24( byte value )
	{
		_ch2Frequency = (_ch2Frequency & 0xFF) | ((value & 7) << 8);
		bool wasStop = _ch2Stop;
		_ch2Stop = (value & 0x40) != 0;

		if ( !wasStop && _ch2Stop && _ch2Length > 0 && (_frameSeqStep & 1) == 0 )
		{
			_ch2Length--;
			if ( _ch2Length == 0 )
				_ch2Playing = false;
		}

		if ( (value & 0x80) != 0 )
		{
			_ch2Playing = ResetEnvelope( ref _ch2EnvVolume, ref _ch2EnvStepTime,
			ref _ch2EnvDirection, ref _ch2EnvInitVolume, ref _ch2EnvDead, ref _ch2EnvNextStep );

			if ( _ch2Length == 0 )
			{
				_ch2Length = 64;
				if ( _ch2Stop && (_frameSeqStep & 1) == 0 )
					_ch2Length--;
			}

			_ch2Sample = DutyTable[_ch2Duty * 8 + _ch2DutyIndex] * _ch2EnvVolume;
			_ch2LastUpdate = _totalCycles;
		}
	}

	private void WriteNR30( byte value )
	{
		_ch3Size = (value & 0x20) != 0;
		_ch3Bank = (value & 0x40) != 0;
		_ch3Enable = (value & 0x80) != 0;
		if ( !_ch3Enable )
		{
			_ch3Playing = false;
		}
	}

	private void WriteNR31( byte value )
	{
		_ch3Length = 256 - value;
	}

	private void WriteNR33( byte value )
	{
		_ch3Rate = (_ch3Rate & 0x700) | value;
	}

	private void WriteNR34( byte value )
	{
		_ch3Rate = (_ch3Rate & 0xFF) | ((value & 7) << 8);
		bool wasStop = _ch3Stop;
		_ch3Stop = (value & 0x40) != 0;

		if ( !wasStop && _ch3Stop && _ch3Length > 0 && (_frameSeqStep & 1) == 0 )
		{
			_ch3Length--;
			if ( _ch3Length == 0 )
				_ch3Playing = false;
		}

		if ( (value & 0x80) != 0 )
		{
			_ch3Playing = _ch3Enable;

			if ( _ch3Length == 0 )
			{
				_ch3Length = 256;
				if ( _ch3Stop && (_frameSeqStep & 1) == 0 )
					_ch3Length--;
			}

			_ch3Window = 0;

			if ( _ch3Playing )
			{
				_ch3NextUpdate = _totalCycles + (6 + 2 * (2048 - _ch3Rate)) * TimingFactor;
			}
		}
	}

	private void WriteNR41( byte value )
	{
		_ch4Length = 64 - (value & 0x3F);
	}

	private void WriteNR42( byte value )
	{
		_ch4EnvStepTime = value & 7;
		_ch4EnvDirection = (value & 8) != 0;
		_ch4EnvInitVolume = (value >> 4) & 0xF;

		if ( _ch4EnvStepTime == 0 )
		{
			_ch4EnvVolume &= 0xF;
		}

		UpdateEnvelopeDead( ref _ch4EnvVolume, ref _ch4EnvStepTime, ref _ch4EnvDirection,
		ref _ch4EnvInitVolume, ref _ch4EnvDead, ref _ch4EnvNextStep );

		if ( _ch4EnvInitVolume == 0 && !_ch4EnvDirection )
		{
			_ch4Playing = false;
		}
	}

	private void WriteNR43( byte value )
	{
		_ch4Ratio = value & 7;
		_ch4Power = (value & 8) != 0;
		_ch4Frequency = (value >> 4) & 0xF;
	}

	private void WriteNR44( byte value )
	{
		bool wasStop = _ch4Stop;
		_ch4Stop = (value & 0x40) != 0;

		if ( !wasStop && _ch4Stop && _ch4Length > 0 && (_frameSeqStep & 1) == 0 )
		{
			_ch4Length--;
			if ( _ch4Length == 0 )
				_ch4Playing = false;
		}

		if ( (value & 0x80) != 0 )
		{
			_ch4Playing = ResetEnvelope( ref _ch4EnvVolume, ref _ch4EnvStepTime,
			ref _ch4EnvDirection, ref _ch4EnvInitVolume, ref _ch4EnvDead, ref _ch4EnvNextStep );

			_ch4Lfsr = 0;

			if ( _ch4Length == 0 )
			{
				_ch4Length = 64;
				if ( _ch4Stop && (_frameSeqStep & 1) == 0 )
					_ch4Length--;
			}

			if ( _ch4Playing )
			{
				_ch4LastEvent = _totalCycles;
			}
		}
	}

	private void WriteNR50( byte value )
	{
		_volumeRight = value & 7;
		_volumeLeft = (value >> 4) & 7;
	}

	private void WriteNR51( byte value )
	{
		_psgCh1Right = (value & 1) != 0;
		_psgCh2Right = (value & 2) != 0;
		_psgCh3Right = (value & 4) != 0;
		_psgCh4Right = (value & 8) != 0;
		_psgCh1Left = (value & 0x10) != 0;
		_psgCh2Left = (value & 0x20) != 0;
		_psgCh3Left = (value & 0x40) != 0;
		_psgCh4Left = (value & 0x80) != 0;
	}

	private void WriteSoundCntH( ushort value )
	{
		SoundCntH = value;
		_psgVolume = value & 3;
		_volumeChA = (value & 4) != 0;
		_volumeChB = (value & 8) != 0;
		_chARight = (value & (1 << 8)) != 0;
		_chALeft = (value & (1 << 9)) != 0;
		_chATimer = (value & (1 << 10)) != 0;
		_chBRight = (value & (1 << 12)) != 0;
		_chBLeft = (value & (1 << 13)) != 0;
		_chBTimer = (value & (1 << 14)) != 0;

		if ( (value & (1 << 11)) != 0 )
		{
			_fifoA.Write = _fifoA.Read = 0;
		}
		if ( (value & (1 << 15)) != 0 )
		{
			_fifoB.Write = _fifoB.Read = 0;
		}
	}

	private void WriteSoundCntX( ushort value )
	{
		bool wasEnabled = Enable;
		bool nowEnabled = (value & 0x80) != 0;
		Enable = nowEnabled;
		SoundCntX = (ushort)((SoundCntX & 0x0F) | (value & 0x80));

		if ( wasEnabled && !nowEnabled )
		{
			ushort[] io = Gba.Memory.Io;
			for ( uint offset = 0x60; offset < 0x82; offset += 2 )
				io[offset >> 1] = 0;

			_ch1Playing = false;
			_ch2Playing = false;
			_ch3Playing = false;
			_ch4Playing = false;

			WriteNR10( 0 );
			WriteNR12( 0 );
			WriteNR13( 0 );
			WriteNR14( 0 );
			WriteNR22( 0 );
			WriteNR23( 0 );
			WriteNR24( 0 );
			WriteNR30( 0 );
			WriteNR33( 0 );
			WriteNR34( 0 );
			WriteNR42( 0 );
			WriteNR43( 0 );
			WriteNR44( 0 );
			WriteNR50( 0 );
			WriteNR51( 0 );

			WriteNR11( 0 );
			WriteNR21( 0 );
			WriteNR31( 0 );
			WriteNR41( 0 );

			Sound1CntL = Sound1CntH = Sound1CntX = 0;
			Sound2CntL = Sound2CntH = 0;
			Sound3CntL = Sound3CntH = Sound3CntX = 0;
			Sound4CntL = Sound4CntH = 0;
			SoundCntL = 0;

			_ch3Size = false;
			_ch3Bank = false;
			_ch3Volume = 0;
			_ch3Sample = 0;

			_psgVolume = 0;
			_volumeChA = false;
			_volumeChB = false;

			SoundCntH &= 0xFF00;
			io[0x82 >> 1] &= 0xFF00;
		}
		else if ( !wasEnabled && nowEnabled )
		{
			_frameSeqStep = 7;
		}
	}

	private static bool ResetEnvelope( ref int volume, ref int stepTime,
	ref bool direction, ref int initVolume, ref int dead, ref int nextStep )
	{
		volume = initVolume;
		nextStep = stepTime;
		UpdateEnvelopeDead( ref volume, ref stepTime, ref direction, ref initVolume, ref dead, ref nextStep );
		return initVolume != 0 || direction;
	}

	private static void UpdateEnvelopeDead( ref int volume, ref int stepTime,
	ref bool direction, ref int initVolume, ref int dead, ref int nextStep )
	{
		if ( stepTime == 0 )
		{
			dead = volume != 0 ? 1 : 2;
		}
		else if ( !direction && volume == 0 )
		{
			dead = 2;
		}
		else if ( direction && volume == 0xF )
		{
			dead = 1;
		}
		else if ( dead != 0 )
		{
			nextStep = stepTime;
			dead = 0;
		}
	}

	private static void TickEnvelope( ref int volume, ref int stepTime,
	ref bool direction, ref int dead, ref int nextStep )
	{
		if ( dead != 0 ) return;
		if ( stepTime == 0 ) return;

		nextStep--;
		if ( nextStep > 0 ) return;

		if ( direction )
		{
			volume++;
			if ( volume >= 15 )
			{
				volume = 15;
				dead = 1;
			}
			else
			{
				nextStep = stepTime;
			}
		}
		else
		{
			volume--;
			if ( volume <= 0 )
			{
				volume = 0;
				dead = 2;
			}
			else
			{
				nextStep = stepTime;
			}
		}
	}

	private bool UpdateSweep( bool initial )
	{
		if ( initial || _ch1SweepTime != 8 )
		{
			int frequency = _ch1SweepRealFreq;
			if ( _ch1SweepDirection )
			{
				frequency -= frequency >> _ch1SweepShift;
				if ( !initial && frequency >= 0 )
				{
					_ch1Frequency = frequency;
					_ch1SweepRealFreq = frequency;
				}
			}
			else
			{
				frequency += frequency >> _ch1SweepShift;
				if ( frequency < 2048 )
				{
					if ( !initial && _ch1SweepShift > 0 )
					{
						_ch1Frequency = frequency;
						_ch1SweepRealFreq = frequency;
						if ( !UpdateSweep( true ) )
							return false;
					}
				}
				else
				{
					return false;
				}
			}
			_ch1SweepOccurred = true;
		}
		_ch1SweepStep = _ch1SweepTime;
		return true;
	}
}