251 lines
8.7 KiB
C#
251 lines
8.7 KiB
C#
using System.Collections.Generic;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace GameBoy.Tests
|
|
{
|
|
public sealed class ApuTests
|
|
{
|
|
private const int SampleRate = 44100;
|
|
private const int CpuClock = 4194304;
|
|
|
|
private readonly ITestOutputHelper output;
|
|
|
|
public ApuTests(ITestOutputHelper output)
|
|
{
|
|
this.output = output;
|
|
}
|
|
|
|
private static Emulator NewMachine()
|
|
{
|
|
return new Emulator(TestRoms.BuildProgram());
|
|
}
|
|
|
|
private static void AdvanceSeconds(Emulator emulator, double seconds)
|
|
{
|
|
long ticks = (long)(seconds * CpuClock) / 4;
|
|
for (long i = 0; i < ticks; i++)
|
|
emulator.Tick4();
|
|
}
|
|
|
|
private static float[] DrainLeft(Emulator emulator)
|
|
{
|
|
int available = emulator.Apu.AvailableSamples;
|
|
var interleaved = new float[available * 2];
|
|
int frames = emulator.Apu.ReadSamples(interleaved);
|
|
var left = new float[frames];
|
|
for (int i = 0; i < frames; i++)
|
|
left[i] = interleaved[i * 2];
|
|
return left;
|
|
}
|
|
|
|
private static void ConfigureSquare1(Emulator emulator, int frequency, byte envelope)
|
|
{
|
|
emulator.WriteMemory(0xFF26, 0x80);
|
|
emulator.WriteMemory(0xFF24, 0x77);
|
|
emulator.WriteMemory(0xFF25, 0x11);
|
|
emulator.WriteMemory(0xFF11, 0x80);
|
|
emulator.WriteMemory(0xFF12, envelope);
|
|
emulator.WriteMemory(0xFF13, (byte)(frequency & 0xFF));
|
|
emulator.WriteMemory(0xFF14, (byte)(0x80 | ((frequency >> 8) & 0x07)));
|
|
}
|
|
|
|
[Fact]
|
|
public void PoweredApuWithNoActiveChannelsIsSilent()
|
|
{
|
|
var emulator = NewMachine();
|
|
emulator.WriteMemory(0xFF26, 0x80);
|
|
emulator.WriteMemory(0xFF24, 0x77);
|
|
emulator.WriteMemory(0xFF25, 0xFF);
|
|
|
|
AdvanceSeconds(emulator, 0.2);
|
|
float[] left = DrainLeft(emulator);
|
|
|
|
Assert.True(left.Length > 8000, $"expected a full sample stream, got {left.Length}");
|
|
foreach (float sample in left)
|
|
Assert.Equal(0f, sample);
|
|
}
|
|
|
|
[Fact]
|
|
public void SquareChannelAmplitudeMatchesMixerMath()
|
|
{
|
|
var emulator = NewMachine();
|
|
ConfigureSquare1(emulator, 1024, 0xF0);
|
|
|
|
AdvanceSeconds(emulator, 0.1);
|
|
float[] left = DrainLeft(emulator);
|
|
|
|
float min = float.MaxValue;
|
|
float max = float.MinValue;
|
|
foreach (float sample in left)
|
|
{
|
|
if (sample < min) min = sample;
|
|
if (sample > max) max = sample;
|
|
}
|
|
|
|
output.WriteLine($"min={min} max={max}");
|
|
Assert.InRange(max, 0.24f, 0.26f);
|
|
Assert.InRange(min, -0.26f, -0.24f);
|
|
}
|
|
|
|
[Fact]
|
|
public void SquareChannelPeriodMatchesProgrammedFrequency()
|
|
{
|
|
var emulator = NewMachine();
|
|
int frequency = 1024;
|
|
ConfigureSquare1(emulator, frequency, 0xF0);
|
|
|
|
AdvanceSeconds(emulator, 0.5);
|
|
float[] left = DrainLeft(emulator);
|
|
|
|
double squareHz = 131072.0 / (2048 - frequency);
|
|
double expectedSamplesPerPeriod = SampleRate / squareHz;
|
|
|
|
double mean = 0;
|
|
foreach (float sample in left)
|
|
mean += sample;
|
|
mean /= left.Length;
|
|
|
|
var crossings = new List<int>();
|
|
for (int i = 1; i < left.Length; i++)
|
|
if (left[i - 1] <= mean && left[i] > mean)
|
|
crossings.Add(i);
|
|
|
|
Assert.True(crossings.Count > 20, $"too few periods detected: {crossings.Count}");
|
|
|
|
double total = 0;
|
|
for (int i = 1; i < crossings.Count; i++)
|
|
total += crossings[i] - crossings[i - 1];
|
|
double measured = total / (crossings.Count - 1);
|
|
|
|
output.WriteLine($"expected {expectedSamplesPerPeriod:F2} samples/period, measured {measured:F2}");
|
|
Assert.InRange(measured, expectedSamplesPerPeriod * 0.97, expectedSamplesPerPeriod * 1.03);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisablingDacSilencesARunningChannel()
|
|
{
|
|
var emulator = NewMachine();
|
|
ConfigureSquare1(emulator, 1024, 0xF0);
|
|
|
|
AdvanceSeconds(emulator, 0.05);
|
|
float[] whilePlaying = DrainLeft(emulator);
|
|
|
|
float loudest = 0;
|
|
foreach (float sample in whilePlaying)
|
|
if (System.MathF.Abs(sample) > loudest)
|
|
loudest = System.MathF.Abs(sample);
|
|
Assert.True(loudest > 0.1f, "channel should be audible before the DAC is disabled");
|
|
|
|
emulator.WriteMemory(0xFF12, 0x00);
|
|
emulator.Apu.ClearSamples();
|
|
|
|
AdvanceSeconds(emulator, 0.05);
|
|
float[] afterMute = DrainLeft(emulator);
|
|
|
|
Assert.True(afterMute.Length > 1000);
|
|
foreach (float sample in afterMute)
|
|
Assert.Equal(0f, sample);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoiseChannelProducesTogglingOutput()
|
|
{
|
|
var emulator = NewMachine();
|
|
emulator.WriteMemory(0xFF26, 0x80);
|
|
emulator.WriteMemory(0xFF24, 0x77);
|
|
emulator.WriteMemory(0xFF25, 0x88);
|
|
emulator.WriteMemory(0xFF20, 0x00);
|
|
emulator.WriteMemory(0xFF21, 0xF0);
|
|
emulator.WriteMemory(0xFF22, 0x00);
|
|
emulator.WriteMemory(0xFF23, 0x80);
|
|
|
|
AdvanceSeconds(emulator, 0.1);
|
|
float[] left = DrainLeft(emulator);
|
|
|
|
float min = float.MaxValue;
|
|
float max = float.MinValue;
|
|
int transitions = 0;
|
|
for (int i = 0; i < left.Length; i++)
|
|
{
|
|
if (left[i] < min) min = left[i];
|
|
if (left[i] > max) max = left[i];
|
|
if (i > 0 && left[i] != left[i - 1])
|
|
transitions++;
|
|
}
|
|
|
|
output.WriteLine($"min={min} max={max} transitions={transitions}");
|
|
Assert.True(max > 0.1f);
|
|
Assert.True(min < -0.1f);
|
|
Assert.True(transitions > 200, $"noise output barely changed: {transitions} transitions");
|
|
}
|
|
|
|
[Fact]
|
|
public void WaveChannelPlaysProgrammedSamples()
|
|
{
|
|
var emulator = NewMachine();
|
|
emulator.WriteMemory(0xFF26, 0x80);
|
|
emulator.WriteMemory(0xFF24, 0x77);
|
|
emulator.WriteMemory(0xFF25, 0x44);
|
|
|
|
byte[] pattern = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 };
|
|
for (int i = 0; i < pattern.Length; i++)
|
|
emulator.WriteMemory((ushort)(0xFF30 + i), pattern[i]);
|
|
|
|
emulator.WriteMemory(0xFF1A, 0x80);
|
|
emulator.WriteMemory(0xFF1B, 0x00);
|
|
emulator.WriteMemory(0xFF1C, 0x20);
|
|
emulator.WriteMemory(0xFF1D, 0x00);
|
|
emulator.WriteMemory(0xFF1E, 0x84);
|
|
|
|
AdvanceSeconds(emulator, 0.1);
|
|
float[] left = DrainLeft(emulator);
|
|
|
|
float min = float.MaxValue;
|
|
float max = float.MinValue;
|
|
foreach (float sample in left)
|
|
{
|
|
if (sample < min) min = sample;
|
|
if (sample > max) max = sample;
|
|
}
|
|
|
|
output.WriteLine($"wave min={min} max={max}");
|
|
Assert.True(max - min > 0.3f, "wave output should swing across the programmed ramp");
|
|
}
|
|
|
|
[Fact]
|
|
public void LengthCounterDisablesChannelWhenEnabled()
|
|
{
|
|
var emulator = NewMachine();
|
|
emulator.WriteMemory(0xFF26, 0x80);
|
|
emulator.WriteMemory(0xFF11, (byte)(0x80 | 63));
|
|
emulator.WriteMemory(0xFF12, 0xF0);
|
|
emulator.WriteMemory(0xFF13, 0x00);
|
|
emulator.WriteMemory(0xFF14, 0xC0);
|
|
|
|
Assert.True((emulator.ReadMemory(0xFF26) & 0x01) != 0, "channel 1 should be active right after trigger");
|
|
|
|
AdvanceSeconds(emulator, 0.02);
|
|
|
|
Assert.True((emulator.ReadMemory(0xFF26) & 0x01) == 0, "channel 1 should have expired via the length counter");
|
|
}
|
|
|
|
[Fact]
|
|
public void PowerOffClearsChannelStatus()
|
|
{
|
|
var emulator = NewMachine();
|
|
ConfigureSquare1(emulator, 1024, 0xF0);
|
|
Assert.True((emulator.ReadMemory(0xFF26) & 0x01) != 0);
|
|
|
|
emulator.WriteMemory(0xFF26, 0x00);
|
|
Assert.Equal(0x70, emulator.ReadMemory(0xFF26));
|
|
|
|
emulator.Apu.ClearSamples();
|
|
AdvanceSeconds(emulator, 0.02);
|
|
float[] left = DrainLeft(emulator);
|
|
foreach (float sample in left)
|
|
Assert.Equal(0f, sample);
|
|
}
|
|
}
|
|
}
|