sharpemu/tests/SharpEmu.Libs.Tests/Audio/AudioOut2MixTests.cs

159 lines
6.3 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Audio;
using Xunit;
namespace SharpEmu.Libs.Tests.Audio;
public sealed class AudioOut2MixTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ushort ObjectMain = 0x100;
private const float HalfPower = 0.70710677f;
[Theory]
[InlineData((ushort)0, true)]
[InlineData((ushort)1, true)]
[InlineData((ushort)2, true)]
[InlineData(ObjectMain, true)]
[InlineData((ushort)3, false)]
[InlineData((ushort)4, false)]
[InlineData((ushort)5, false)]
[InlineData((ushort)6, false)]
public void OnlyMainBgmAndVoicePortsReachTheSpeakers(ushort portType, bool expected)
{
Assert.Equal(expected, AudioOut2Exports.RoutesToSpeakers(portType));
Assert.Equal(expected, AudioOut2Exports.TryGetStereoGains(portType, 2, 1f, AudioOut2Exports.AmbisonicsNone, 0, out _, out _));
}
[Theory]
[InlineData(64u, HalfPower, HalfPower)]
[InlineData(65u, HalfPower, -HalfPower)]
[InlineData(0u, 1f, 1f)]
[InlineData(2u, HalfPower, -HalfPower)]
public void FirstOrderAmbisonicsDecodeToStereoCardioids(uint ambisonics, float left, float right)
{
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 0.5f, ambisonics, 0, out var leftGain, out var rightGain));
Assert.Equal(left * 0.5f, leftGain, 5);
Assert.Equal(right * 0.5f, rightGain, 5);
}
[Theory]
[InlineData(66u)]
[InlineData(67u)]
[InlineData(99u)]
[InlineData(1u)]
[InlineData(3u)]
[InlineData(15u)]
public void HigherAmbisonicComponentsDoNotFeedStereo(uint ambisonics)
{
Assert.False(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, ambisonics, 0, out _, out _));
}
[Fact]
public void PlainObjectsPlayCentredOrPassThroughToOneSide()
{
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, 0, out var left, out var right));
Assert.Equal(HalfPower, left, 5);
Assert.Equal(HalfPower, right, 5);
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, AudioOut2Exports.PassthroughLeft, out left, out right));
Assert.Equal((1f, 0f), (left, right));
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, AudioOut2Exports.PassthroughRight, out left, out right));
Assert.Equal((0f, 1f), (left, right));
Assert.False(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 0f, AudioOut2Exports.AmbisonicsNone, 0, out _, out _));
}
[Fact]
public void FifthOrderSceneDecodesASourceToItsSide()
{
const int frames = 4;
var leftSource = DecodeScene(frames, azimuthSine: 1f);
var centreSource = DecodeScene(frames, azimuthSine: 0f);
Assert.Equal(2f * HalfPower, leftSource[0], 4);
Assert.Equal(0f, leftSource[1], 4);
Assert.Equal(HalfPower, centreSource[0], 4);
Assert.Equal(HalfPower, centreSource[1], 4);
}
[Fact]
public void SetAttributesStoresGainAmbisonicsAndPassthrough()
{
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
var ctx = new CpuContext(memory, Generation.Gen5);
var param = new byte[0x40];
BinaryPrimitives.WriteUInt16LittleEndian(param, ObjectMain);
BinaryPrimitives.WriteUInt32LittleEndian(param.AsSpan(4), 0x100);
Assert.True(memory.TryWrite(MemoryBase, param));
ctx[CpuRegister.Rdi] = 0;
ctx[CpuRegister.Rsi] = MemoryBase;
ctx[CpuRegister.Rdx] = MemoryBase + 0x80;
Assert.Equal(0, AudioOut2Exports.AudioOut2PortCreate(ctx));
var handle = BinaryPrimitives.ReadUInt64LittleEndian(Read(memory, MemoryBase + 0x80, 8));
var values = new byte[12];
BinaryPrimitives.WriteSingleLittleEndian(values, 0.25f);
BinaryPrimitives.WriteUInt32LittleEndian(values.AsSpan(4), 65);
BinaryPrimitives.WriteUInt32LittleEndian(values.AsSpan(8), AudioOut2Exports.PassthroughLeft);
Assert.True(memory.TryWrite(MemoryBase + 0x100, values));
var attributes = new byte[0x18 * 3];
WriteAttribute(attributes, 0, 1, MemoryBase + 0x100);
WriteAttribute(attributes, 1, 8, MemoryBase + 0x104);
WriteAttribute(attributes, 2, 5, MemoryBase + 0x108);
Assert.True(memory.TryWrite(MemoryBase + 0x200, attributes));
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = MemoryBase + 0x200;
ctx[CpuRegister.Rdx] = 3;
Assert.Equal(0, AudioOut2Exports.AudioOut2PortSetAttributes(ctx));
Assert.Equal((0.25f, 65u, AudioOut2Exports.PassthroughLeft), AudioOut2Exports.PortMixAttributes(handle));
ctx[CpuRegister.Rdi] = handle;
AudioOut2Exports.AudioOut2PortDestroy(ctx);
}
private static float[] DecodeScene(int frames, float azimuthSine)
{
var mix = new float[frames * 2];
var source = new byte[frames * sizeof(float)];
for (var acn = 0u; acn < 36; acn++)
{
var component = acn switch
{
0 => 1f,
1 => azimuthSine,
_ => 0.6f,
};
for (var frame = 0; frame < frames; frame++)
{
BinaryPrimitives.WriteSingleLittleEndian(source.AsSpan(frame * sizeof(float)), component);
}
if (AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsAcnBase + acn, 0, out var left, out var right))
{
AudioOut2Exports.MixPortIntoStereo(source, mix, frames, 1, sizeof(float), true, left, right);
}
}
return mix;
}
private static void WriteAttribute(byte[] attributes, int index, uint id, ulong value)
{
var entry = attributes.AsSpan(index * 0x18, 0x18);
BinaryPrimitives.WriteUInt32LittleEndian(entry, id);
BinaryPrimitives.WriteUInt64LittleEndian(entry[0x08..], value);
BinaryPrimitives.WriteUInt64LittleEndian(entry[0x10..], sizeof(uint));
}
private static byte[] Read(FakeCpuMemory memory, ulong address, int length)
{
var bytes = new byte[length];
Assert.True(memory.TryRead(address, bytes));
return bytes;
}
}