test(audio): add APU register and channel tests
This commit is contained in:
1 parent
0127f25af7
commit
39460ef112
1 file changed
+250
@@ -0,0 +1,250 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user