Emulator/GbaAudio.cs
using System.IO;
namespace sGBA;
public partial class GbaAudio
{
public Gba Gba { get; }
public const int SampleRate = 32768;
public const int CyclesPerSample = GbaConstants.Arm7TdmiFrequency / SampleRate;
public const int SamplesPerFrame = (GbaConstants.VideoTotalLength + CyclesPerSample - 1) / CyclesPerSample;
public short[] OutputBuffer { get; set; } = new short[SamplesPerFrame * 2];
public int SamplesWritten { get; set; }
private long _nextSampleCycle;
private long _nextFrameSeqCycle;
private readonly GbaTimingEvent _sampleEvent;
private readonly GbaTimingEvent _frameSeqEvent;
private const int TimingFactor = 4;
private const int FrameCycles = 0x2000;
private const int CyclesPerFrameSeq = TimingFactor * FrameCycles;
private int _frameSeqStep;
private long _totalCycles;
public bool Enable;
public ushort Sound1CntL, Sound1CntH, Sound1CntX;
public ushort Sound2CntL, Sound2CntH;
public ushort Sound3CntL, Sound3CntH, Sound3CntX;
public ushort Sound4CntL, Sound4CntH;
public ushort SoundCntL, SoundCntH, SoundCntX;
public ushort SoundBias;
private int _volumeRight;
private int _volumeLeft;
private bool _psgCh1Right, _psgCh2Right, _psgCh3Right, _psgCh4Right;
private bool _psgCh1Left, _psgCh2Left, _psgCh3Left, _psgCh4Left;
private int _psgVolume;
private bool _volumeChA;
private bool _volumeChB;
private bool _chARight;
private bool _chALeft;
private bool _chATimer;
private bool _chBRight;
private bool _chBLeft;
private bool _chBTimer;
public byte[] WaveRam = new byte[32];
private struct FifoState
{
public uint[] Buffer;
public int Write, Read;
public uint Internal;
public int Remaining;
public sbyte Sample;
}
private FifoState _fifoA;
private FifoState _fifoB;
private bool _ch1Playing;
private int _ch1Frequency;
private int _ch1Length;
private bool _ch1Stop;
private int _ch1DutyIndex;
private int _ch1Duty;
private int _ch1Sample;
private long _ch1LastUpdate;
private int _ch1EnvVolume;
private int _ch1EnvStepTime;
private bool _ch1EnvDirection;
private int _ch1EnvInitVolume;
private int _ch1EnvDead;
private int _ch1EnvNextStep;
private int _ch1SweepShift;
private bool _ch1SweepDirection;
private int _ch1SweepTime;
private int _ch1SweepStep;
private bool _ch1SweepEnable;
private bool _ch1SweepOccurred;
private int _ch1SweepRealFreq;
private bool _ch2Playing;
private int _ch2Frequency;
private int _ch2Length;
private bool _ch2Stop;
private int _ch2DutyIndex;
private int _ch2Duty;
private int _ch2Sample;
private long _ch2LastUpdate;
private int _ch2EnvVolume;
private int _ch2EnvStepTime;
private bool _ch2EnvDirection;
private int _ch2EnvInitVolume;
private int _ch2EnvDead;
private int _ch2EnvNextStep;
private bool _ch3Playing;
private bool _ch3Enable;
private bool _ch3Size;
private bool _ch3Bank;
private int _ch3Volume;
private int _ch3Rate;
private int _ch3Length;
private bool _ch3Stop;
private int _ch3Window;
private int _ch3Sample;
private long _ch3NextUpdate;
private bool _ch4Playing;
private int _ch4Ratio;
private int _ch4Frequency;
private bool _ch4Power;
private int _ch4Length;
private bool _ch4Stop;
private uint _ch4Lfsr;
private int _ch4Sample;
private long _ch4LastEvent;
private int _ch4EnvVolume;
private int _ch4EnvStepTime;
private bool _ch4EnvDirection;
private int _ch4EnvInitVolume;
private int _ch4EnvDead;
private int _ch4EnvNextStep;
private static readonly int[] DutyTable =
[
0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
];
public GbaAudio( Gba gba )
{
Gba = gba;
SoundBias = 0x0200;
_fifoA.Buffer = new uint[8];
_fifoB.Buffer = new uint[8];
_frameSeqEvent = new GbaTimingEvent( OnFrameSeqEvent, 5, "audio-frameseq" );
_sampleEvent = new GbaTimingEvent( OnSampleEvent, 6, "audio-sample" );
}
public void Reset()
{
SamplesWritten = 0;
_frameSeqStep = 0;
_totalCycles = Gba.Cpu.Cycles;
_nextSampleCycle = Gba.Cpu.Cycles + CyclesPerSample;
_nextFrameSeqCycle = Gba.Cpu.Cycles;
ScheduleAudioEvents();
Sound1CntL = Sound1CntH = Sound1CntX = 0;
Sound2CntL = Sound2CntH = 0;
Sound3CntL = Sound3CntH = Sound3CntX = 0;
Sound4CntL = Sound4CntH = 0;
SoundCntL = SoundCntH = SoundCntX = 0;
SoundBias = 0x0200;
Enable = false;
_psgVolume = 0;
_volumeChA = false;
_volumeChB = false;
_chARight = _chALeft = false;
_chATimer = false;
_chBRight = _chBLeft = false;
_chBTimer = false;
_volumeRight = _volumeLeft = 0;
_psgCh1Right = _psgCh2Right = _psgCh3Right = _psgCh4Right = false;
_psgCh1Left = _psgCh2Left = _psgCh3Left = _psgCh4Left = false;
Array.Clear( WaveRam );
ResetFifo( true, true );
ResetFifo( false, true );
_ch1Playing = _ch2Playing = _ch3Playing = _ch4Playing = false;
_ch1EnvDead = _ch2EnvDead = _ch4EnvDead = 2;
_ch1SweepTime = 8;
_ch1EnvVolume = _ch2EnvVolume = _ch4EnvVolume = 0;
_ch1EnvInitVolume = _ch2EnvInitVolume = _ch4EnvInitVolume = 0;
_ch1EnvStepTime = _ch2EnvStepTime = _ch4EnvStepTime = 0;
_ch1EnvDirection = _ch2EnvDirection = _ch4EnvDirection = false;
_ch1EnvNextStep = _ch2EnvNextStep = _ch4EnvNextStep = 0;
_ch1Frequency = _ch2Frequency = 0;
_ch1Length = _ch2Length = _ch3Length = _ch4Length = 0;
_ch1Stop = _ch2Stop = _ch3Stop = _ch4Stop = false;
_ch1DutyIndex = _ch2DutyIndex = 0;
_ch1Duty = _ch2Duty = 0;
_ch1Sample = _ch2Sample = _ch3Sample = _ch4Sample = 0;
_ch1LastUpdate = _ch2LastUpdate = 0;
_ch3NextUpdate = 0;
_ch4LastEvent = 0;
_ch1SweepShift = 0;
_ch1SweepDirection = false;
_ch1SweepStep = 0;
_ch1SweepEnable = false;
_ch1SweepOccurred = false;
_ch1SweepRealFreq = 0;
_ch3Enable = false;
_ch3Size = false;
_ch3Bank = false;
_ch3Volume = 0;
_ch3Rate = 0;
_ch3Window = 0;
_ch4Ratio = 0;
_ch4Frequency = 0;
_ch4Power = false;
_ch4Lfsr = 0;
}
public void ResetFifo( bool isA, bool clearLatched )
{
ref var fifo = ref isA ? ref _fifoA : ref _fifoB;
Array.Clear( fifo.Buffer );
fifo.Write = fifo.Read = 0;
fifo.Internal = 0;
fifo.Remaining = 0;
if ( clearLatched ) fifo.Sample = 0;
}
public void BeginFrame()
{
SamplesWritten = 0;
}
public void WriteFifo( bool isA, uint value )
{
ref var fifo = ref isA ? ref _fifoA : ref _fifoB;
fifo.Buffer[fifo.Write] = value;
fifo.Write = (fifo.Write + 1) & 7;
}
public void OnTimerOverflow( int timer )
{
if ( !Enable ) return;
if ( (_chALeft || _chARight) && (_chATimer ? 1 : 0) == timer )
SampleFifo( ref _fifoA, 1 );
if ( (_chBLeft || _chBRight) && (_chBTimer ? 1 : 0) == timer )
SampleFifo( ref _fifoB, 2 );
}
private void SampleFifo( ref FifoState fifo, int dmaChannel )
{
int size = FifoSize( ref fifo );
if ( 8 - size > 4 )
{
Gba.Dma.OnFifo( dmaChannel );
}
if ( fifo.Remaining == 0 && size > 0 )
{
fifo.Internal = fifo.Buffer[fifo.Read];
fifo.Remaining = 4;
fifo.Read = (fifo.Read + 1) & 7;
}
if ( fifo.Remaining > 0 )
{
fifo.Sample = (sbyte)(fifo.Internal & 0xFF);
fifo.Internal >>= 8;
fifo.Remaining--;
}
}
private static int FifoSize( ref FifoState fifo )
{
return fifo.Write >= fifo.Read
? fifo.Write - fifo.Read
: 8 - fifo.Read + fifo.Write;
}
public void FlushSamples()
{
RunPsg( Gba.Cpu.Cycles );
}
private void OnSampleEvent( long late )
{
long when = Gba.Cpu.Cycles - late;
while ( _nextSampleCycle <= when )
{
_totalCycles = _nextSampleCycle;
WriteSample();
_nextSampleCycle += CyclesPerSample;
}
Gba.Timing.Schedule( _sampleEvent, _nextSampleCycle + CyclesPerSample );
}
private void OnFrameSeqEvent( long late )
{
_totalCycles = Gba.Cpu.Cycles - late;
ClockFrameSequencer();
_nextFrameSeqCycle = _totalCycles + CyclesPerFrameSeq;
Gba.Timing.Schedule( _frameSeqEvent, _nextFrameSeqCycle );
}
internal void ScheduleAudioEvents()
{
Gba.Timing.Schedule( _sampleEvent, _nextSampleCycle + CyclesPerSample );
Gba.Timing.Schedule( _frameSeqEvent, _nextFrameSeqCycle );
}
private void WriteSample()
{
if ( SamplesWritten >= SamplesPerFrame )
return;
short left;
short right;
if ( Enable )
MixSample( out left, out right );
else
{
left = 0;
right = 0;
}
OutputBuffer[SamplesWritten * 2] = left;
OutputBuffer[SamplesWritten * 2 + 1] = right;
SamplesWritten++;
}
public void Serialize( BinaryWriter w )
{
w.Write( _nextSampleCycle );
w.Write( _nextFrameSeqCycle );
w.Write( _frameSeqStep );
w.Write( _totalCycles );
w.Write( _volumeRight ); w.Write( _volumeLeft );
w.Write( _psgCh1Right ); w.Write( _psgCh2Right ); w.Write( _psgCh3Right ); w.Write( _psgCh4Right );
w.Write( _psgCh1Left ); w.Write( _psgCh2Left ); w.Write( _psgCh3Left ); w.Write( _psgCh4Left );
w.Write( _psgVolume );
w.Write( _volumeChA ); w.Write( _volumeChB );
w.Write( _chARight ); w.Write( _chALeft ); w.Write( _chATimer );
w.Write( _chBRight ); w.Write( _chBLeft ); w.Write( _chBTimer );
WriteFifo( w, ref _fifoA );
WriteFifo( w, ref _fifoB );
w.Write( _ch1Playing ); w.Write( _ch1Frequency ); w.Write( _ch1Length ); w.Write( _ch1Stop );
w.Write( _ch1DutyIndex ); w.Write( _ch1Duty ); w.Write( _ch1Sample ); w.Write( _ch1LastUpdate );
w.Write( _ch1EnvVolume ); w.Write( _ch1EnvStepTime ); w.Write( _ch1EnvDirection );
w.Write( _ch1EnvInitVolume ); w.Write( _ch1EnvDead ); w.Write( _ch1EnvNextStep );
w.Write( _ch1SweepShift ); w.Write( _ch1SweepDirection ); w.Write( _ch1SweepTime );
w.Write( _ch1SweepStep ); w.Write( _ch1SweepEnable ); w.Write( _ch1SweepOccurred ); w.Write( _ch1SweepRealFreq );
w.Write( _ch2Playing ); w.Write( _ch2Frequency ); w.Write( _ch2Length ); w.Write( _ch2Stop );
w.Write( _ch2DutyIndex ); w.Write( _ch2Duty ); w.Write( _ch2Sample ); w.Write( _ch2LastUpdate );
w.Write( _ch2EnvVolume ); w.Write( _ch2EnvStepTime ); w.Write( _ch2EnvDirection );
w.Write( _ch2EnvInitVolume ); w.Write( _ch2EnvDead ); w.Write( _ch2EnvNextStep );
w.Write( _ch3Playing ); w.Write( _ch3Enable ); w.Write( _ch3Size ); w.Write( _ch3Bank );
w.Write( _ch3Volume ); w.Write( _ch3Rate ); w.Write( _ch3Length ); w.Write( _ch3Stop );
w.Write( _ch3Window ); w.Write( _ch3Sample ); w.Write( _ch3NextUpdate );
w.Write( _ch4Playing ); w.Write( _ch4Ratio ); w.Write( _ch4Frequency ); w.Write( _ch4Power );
w.Write( _ch4Length ); w.Write( _ch4Stop ); w.Write( _ch4Lfsr ); w.Write( _ch4Sample ); w.Write( _ch4LastEvent );
w.Write( _ch4EnvVolume ); w.Write( _ch4EnvStepTime ); w.Write( _ch4EnvDirection );
w.Write( _ch4EnvInitVolume ); w.Write( _ch4EnvDead ); w.Write( _ch4EnvNextStep );
}
public void Deserialize( BinaryReader r )
{
_nextSampleCycle = r.ReadInt64();
_nextFrameSeqCycle = r.ReadInt64();
_frameSeqStep = r.ReadInt32();
_totalCycles = r.ReadInt64();
_volumeRight = r.ReadInt32(); _volumeLeft = r.ReadInt32();
_psgCh1Right = r.ReadBoolean(); _psgCh2Right = r.ReadBoolean(); _psgCh3Right = r.ReadBoolean(); _psgCh4Right = r.ReadBoolean();
_psgCh1Left = r.ReadBoolean(); _psgCh2Left = r.ReadBoolean(); _psgCh3Left = r.ReadBoolean(); _psgCh4Left = r.ReadBoolean();
_psgVolume = r.ReadInt32();
_volumeChA = r.ReadBoolean(); _volumeChB = r.ReadBoolean();
_chARight = r.ReadBoolean(); _chALeft = r.ReadBoolean(); _chATimer = r.ReadBoolean();
_chBRight = r.ReadBoolean(); _chBLeft = r.ReadBoolean(); _chBTimer = r.ReadBoolean();
ReadFifo( r, ref _fifoA );
ReadFifo( r, ref _fifoB );
_ch1Playing = r.ReadBoolean(); _ch1Frequency = r.ReadInt32(); _ch1Length = r.ReadInt32(); _ch1Stop = r.ReadBoolean();
_ch1DutyIndex = r.ReadInt32(); _ch1Duty = r.ReadInt32(); _ch1Sample = r.ReadInt32(); _ch1LastUpdate = r.ReadInt64();
_ch1EnvVolume = r.ReadInt32(); _ch1EnvStepTime = r.ReadInt32(); _ch1EnvDirection = r.ReadBoolean();
_ch1EnvInitVolume = r.ReadInt32(); _ch1EnvDead = r.ReadInt32(); _ch1EnvNextStep = r.ReadInt32();
_ch1SweepShift = r.ReadInt32(); _ch1SweepDirection = r.ReadBoolean(); _ch1SweepTime = r.ReadInt32();
_ch1SweepStep = r.ReadInt32(); _ch1SweepEnable = r.ReadBoolean(); _ch1SweepOccurred = r.ReadBoolean(); _ch1SweepRealFreq = r.ReadInt32();
_ch2Playing = r.ReadBoolean(); _ch2Frequency = r.ReadInt32(); _ch2Length = r.ReadInt32(); _ch2Stop = r.ReadBoolean();
_ch2DutyIndex = r.ReadInt32(); _ch2Duty = r.ReadInt32(); _ch2Sample = r.ReadInt32(); _ch2LastUpdate = r.ReadInt64();
_ch2EnvVolume = r.ReadInt32(); _ch2EnvStepTime = r.ReadInt32(); _ch2EnvDirection = r.ReadBoolean();
_ch2EnvInitVolume = r.ReadInt32(); _ch2EnvDead = r.ReadInt32(); _ch2EnvNextStep = r.ReadInt32();
_ch3Playing = r.ReadBoolean(); _ch3Enable = r.ReadBoolean(); _ch3Size = r.ReadBoolean(); _ch3Bank = r.ReadBoolean();
_ch3Volume = r.ReadInt32(); _ch3Rate = r.ReadInt32(); _ch3Length = r.ReadInt32(); _ch3Stop = r.ReadBoolean();
_ch3Window = r.ReadInt32(); _ch3Sample = r.ReadInt32(); _ch3NextUpdate = r.ReadInt64();
_ch4Playing = r.ReadBoolean(); _ch4Ratio = r.ReadInt32(); _ch4Frequency = r.ReadInt32(); _ch4Power = r.ReadBoolean();
_ch4Length = r.ReadInt32(); _ch4Stop = r.ReadBoolean(); _ch4Lfsr = r.ReadUInt32(); _ch4Sample = r.ReadInt32(); _ch4LastEvent = r.ReadInt64();
_ch4EnvVolume = r.ReadInt32(); _ch4EnvStepTime = r.ReadInt32(); _ch4EnvDirection = r.ReadBoolean();
_ch4EnvInitVolume = r.ReadInt32(); _ch4EnvDead = r.ReadInt32(); _ch4EnvNextStep = r.ReadInt32();
}
private static void WriteFifo( BinaryWriter w, ref FifoState fifo )
{
for ( int i = 0; i < 8; i++ ) w.Write( fifo.Buffer[i] );
w.Write( fifo.Write ); w.Write( fifo.Read );
w.Write( fifo.Internal ); w.Write( fifo.Remaining );
w.Write( fifo.Sample );
}
private static void ReadFifo( BinaryReader r, ref FifoState fifo )
{
for ( int i = 0; i < 8; i++ ) fifo.Buffer[i] = r.ReadUInt32();
fifo.Write = r.ReadInt32(); fifo.Read = r.ReadInt32();
fifo.Internal = r.ReadUInt32(); fifo.Remaining = r.ReadInt32();
fifo.Sample = r.ReadSByte();
}
}